# 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", }), ); ```