11 KiB
11 KiB
Angular Routing Patterns
Table of Contents
- Route Configuration Options
- Authentication Flow
- Breadcrumbs
- Tab Navigation
- Modal Routes
- Preloading Strategies
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',
}),
);