Files
NGRX-Playground/.agents/skills/angular-routing/references/routing-patterns.md
T

11 KiB

Angular Routing Patterns

Table of Contents

Route Configuration Options

Full Route Options

{
  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

export const userTitleResolver: ResolveFn<string> = (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

// auth.service.ts
@Injectable({ providedIn: 'root' })
export class Auth {
    private _user = signal<User | null>(null);
    private _token = signal<string | null>(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<boolean> {
        try {
            const response = await firstValueFrom(this.http.post<AuthResponse>('/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<boolean> {
        const token = localStorage.getItem('token');
        if (!token) return false;

        try {
            const user = await firstValueFrom(this.http.get<User>('/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: `
        <form (ngSubmit)="login()">
            <input
                name="email"
                [(ngModel)]="email" />
            <input
                name="password"
                type="password"
                [(ngModel)]="password" />
            <button type="submit">Login</button>
        </form>
    `,
})
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

// 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: `
        <nav aria-label="Breadcrumb">
            <ol>
                <li><a routerLink="/">Home</a></li>
                @for (crumb of breadcrumbService.breadcrumbs(); track crumb.url) {
                    <li>
                        <a [routerLink]="crumb.url">{{ crumb.label }}</a>
                    </li>
                }
            </ol>
        </nav>
    `,
})
export class BreadcrumbCmpt {
    breadcrumbService = inject(Breadcrumb);
}

Tab Navigation

// tabs-layout.component.ts
@Component({
  imports: [RouterOutlet, RouterLink, RouterLinkActive],
  template: `
    <div class="tabs">
      @for (tab of tabs; track tab.path) {
        <a
          [routerLink]="tab.path"
          routerLinkActive="active"
          [routerLinkActiveOptions]="{ exact: tab.exact }"
        >
          {{ tab.label }}
        </a>
      }
    </div>
    <div class="tab-content">
      <router-outlet />
    </div>
  `,
})
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:

// Routes
export const routes: Routes = [
  { path: 'products', component: ProductList },
  { path: 'product-modal/:id', component: ProductModal, outlet: 'modal' },
];

// App template
@Component({
  template: `
    <router-outlet />
    <router-outlet name="modal" />
  `,
})
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
<a [routerLink]="[{ outlets: { modal: ['product-modal', product.id] } }]">
  View Details
</a>

Preloading Strategies

Built-in Strategies

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

// selective-preload.strategy.ts
@Injectable({ providedIn: 'root' })
export class SelectivePreloadStrategy implements PreloadingStrategy {
  preload(route: Route, load: () => Observable<any>): Observable<any> {
    // 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

@Injectable({ providedIn: 'root' })
export class NetworkAwarePreloadStrategy implements PreloadingStrategy {
    preload(route: Route, load: () => Observable<any>): Observable<any> {
        // 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

// 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: `
        <div [@routeAnimations]="getRouteAnimationData()">
            <router-outlet />
        </div>
    `,
    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

// app.config.ts
import { provideRouter, withInMemoryScrolling, withRouterConfig } from '@angular/router';

provideRouter(
    routes,
    withInMemoryScrolling({
        scrollPositionRestoration: 'enabled', // or 'top'
        anchorScrolling: 'enabled',
    }),
    withRouterConfig({
        onSameUrlNavigation: 'reload',
    }),
);