# Angular Routing Patterns ## Table of Contents - [Route Configuration Options](#route-configuration-options) - [Authentication Flow](#authentication-flow) - [Breadcrumbs](#breadcrumbs) - [Tab Navigation](#tab-navigation) - [Modal Routes](#modal-routes) - [Preloading Strategies](#preloading-strategies) ## Route Configuration Options ### Full Route Options ```typescript { path: 'users/:id', component: UserCmpt, // Lazy loading alternatives loadComponent: () => import('./user.component').then(m => m.UserCmpt), loadChildren: () => import('./user.routes').then(m => m.userRoutes), // Guards canActivate: [authGuard], canActivateChild: [authGuard], canDeactivate: [unsavedChangesGuard], canMatch: [featureFlagGuard], // Data resolve: { user: userResolver }, data: { title: 'User Profile', animation: 'userPage' }, // Children children: [...], // Outlet outlet: 'sidebar', // Path matching pathMatch: 'full', // or 'prefix' // Title title: 'User Profile', // Or dynamic title title: userTitleResolver, } ``` ### Dynamic Title Resolver ```typescript export const userTitleResolver: ResolveFn = (route) => { const userService = inject(User); const id = route.paramMap.get('id')!; return userService.getById(id).pipe(map((user) => `${user.name} - Profile`)); }; ``` ## Authentication Flow ### Complete Auth Setup ```typescript // auth.service.ts @Injectable({ providedIn: 'root' }) export class Auth { private _user = signal(null); private _token = signal(null); readonly user = this._user.asReadonly(); readonly isAuthenticated = computed(() => this._user() !== null); private router = inject(Router); private http = inject(HttpClient); async login(credentials: Credentials): Promise { try { const response = await firstValueFrom(this.http.post('/api/login', credentials)); this._token.set(response.token); this._user.set(response.user); localStorage.setItem('token', response.token); return true; } catch { return false; } } logout(): void { this._user.set(null); this._token.set(null); localStorage.removeItem('token'); this.router.navigate(['/login']); } async checkAuth(): Promise { const token = localStorage.getItem('token'); if (!token) return false; try { const user = await firstValueFrom(this.http.get('/api/me')); this._user.set(user); this._token.set(token); return true; } catch { localStorage.removeItem('token'); return false; } } } // auth.guard.ts export const authGuard: CanActivateFn = async (route, state) => { const authService = inject(Auth); const router = inject(Router); // Check if already authenticated if (authService.isAuthenticated()) { return true; } // Try to restore session const isValid = await authService.checkAuth(); if (isValid) { return true; } // Redirect to login return router.createUrlTree(['/login'], { queryParams: { returnUrl: state.url }, }); }; // login.component.ts @Component({ template: `
`, }) export class Login { private authService = inject(Auth); private router = inject(Router); private route = inject(ActivatedRoute); email = ''; password = ''; async login() { const success = await this.authService.login({ email: this.email, password: this.password, }); if (success) { const returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/'; this.router.navigateByUrl(returnUrl); } } } ``` ## Breadcrumbs ```typescript // breadcrumb.service.ts @Injectable({ providedIn: 'root' }) export class Breadcrumb { private router = inject(Router); private route = inject(ActivatedRoute); breadcrumbs = toSignal( this.router.events.pipe( filter((event) => event instanceof NavigationEnd), map(() => this.buildBreadcrumbs(this.route.root)), ), { initialValue: [] }, ); private buildBreadcrumbs(route: ActivatedRoute, url: string = '', breadcrumbs: Breadcrumb[] = []): Breadcrumb[] { const children = route.children; if (children.length === 0) { return breadcrumbs; } for (const child of children) { const routeUrl = child.snapshot.url.map((segment) => segment.path).join('/'); if (routeUrl) { url += `/${routeUrl}`; } const label = child.snapshot.data['breadcrumb']; if (label) { breadcrumbs.push({ label, url }); } return this.buildBreadcrumbs(child, url, breadcrumbs); } return breadcrumbs; } } // Route config with breadcrumb data export const routes: Routes = [ { path: 'products', data: { breadcrumb: 'Products' }, children: [ { path: '', component: ProductList }, { path: ':id', data: { breadcrumb: 'Product Details' }, component: ProductDetail, }, ], }, ]; // breadcrumb.component.ts @Component({ selector: 'app-breadcrumb', template: ` `, }) export class BreadcrumbCmpt { breadcrumbService = inject(Breadcrumb); } ``` ## Tab Navigation ```typescript // tabs-layout.component.ts @Component({ imports: [RouterOutlet, RouterLink, RouterLinkActive], template: `
@for (tab of tabs; track tab.path) { {{ tab.label }} }
`, }) export class TabsLayout { tabs = [ { path: './', label: 'Overview', exact: true }, { path: 'details', label: 'Details', exact: false }, { path: 'settings', label: 'Settings', exact: false }, ]; } // Routes { path: 'account', component: TabsLayout, children: [ { path: '', component: AccountOverview }, { path: 'details', component: AccountDetails }, { path: 'settings', component: AccountSettings }, ], } ``` ## Modal Routes Using auxiliary outlets for modals: ```typescript // Routes export const routes: Routes = [ { path: 'products', component: ProductList }, { path: 'product-modal/:id', component: ProductModal, outlet: 'modal' }, ]; // App template @Component({ template: ` `, }) export class App {} // Open modal this.router.navigate([{ outlets: { modal: ['product-modal', productId] } }]); // Close modal this.router.navigate([{ outlets: { modal: null } }]); // Link to open modal View Details ``` ## Preloading Strategies ### Built-in Strategies ```typescript import { provideRouter, withPreloading, PreloadAllModules, NoPreloading } from '@angular/router'; // Preload all lazy modules provideRouter(routes, withPreloading(PreloadAllModules)); // No preloading (default) provideRouter(routes, withPreloading(NoPreloading)); ``` ### Custom Preloading Strategy ```typescript // selective-preload.strategy.ts @Injectable({ providedIn: 'root' }) export class SelectivePreloadStrategy implements PreloadingStrategy { preload(route: Route, load: () => Observable): Observable { // Only preload routes marked with data.preload = true if (route.data?.['preload']) { return load(); } return of(null); } } // Routes { path: 'dashboard', loadComponent: () => import('./dashboard.component'), data: { preload: true }, // Will be preloaded } // Config provideRouter(routes, withPreloading(SelectivePreloadStrategy)) ``` ### Network-Aware Preloading ```typescript @Injectable({ providedIn: 'root' }) export class NetworkAwarePreloadStrategy implements PreloadingStrategy { preload(route: Route, load: () => Observable): Observable { // Check network conditions const connection = (navigator as any).connection; if (connection) { // Don't preload on slow connections if (connection.saveData || connection.effectiveType === '2g') { return of(null); } } // Preload if marked if (route.data?.['preload']) { return load(); } return of(null); } } ``` ## Route Animations ```typescript // app.routes.ts export const routes: Routes = [ { path: 'home', component: Home, data: { animation: 'HomePage' } }, { path: 'about', component: About, data: { animation: 'AboutPage' } }, ]; // app.component.ts @Component({ imports: [RouterOutlet], template: `
`, animations: [ trigger('routeAnimations', [ transition('HomePage <=> AboutPage', [ style({ position: 'relative' }), query(':enter, :leave', [ style({ position: 'absolute', top: 0, left: 0, width: '100%', }), ]), query(':enter', [style({ left: '-100%' })]), query(':leave', animateChild()), group([ query(':leave', [animate('300ms ease-out', style({ left: '100%' }))]), query(':enter', [animate('300ms ease-out', style({ left: '0%' }))]), ]), ]), ]), ], }) export class AppMain { getRouteAnimationData() { return this.route.firstChild?.snapshot.data['animation']; } } ``` ## Scroll Position Restoration ```typescript // app.config.ts import { provideRouter, withInMemoryScrolling, withRouterConfig } from '@angular/router'; provideRouter( routes, withInMemoryScrolling({ scrollPositionRestoration: 'enabled', // or 'top' anchorScrolling: 'enabled', }), withRouterConfig({ onSameUrlNavigation: 'reload', }), ); ```