19 Routing in Angular

19.1 Grundkonzepte des Angular Routings

Routing ist ein fundamentaler Bestandteil jeder Single Page Application (SPA) wie Angular. Es ermöglicht die Navigation zwischen verschiedenen Ansichten oder Komponenten, ohne dass die gesamte Seite neu geladen werden muss. Angular bietet ein leistungsfähiges Routing-Modul, das die Erstellung komplexer Navigationsstrukturen vereinfacht.

19.1.1 Das Router-Modul einrichten

Um das Routing in einer Angular-Anwendung zu implementieren, müssen wir zunächst das RouterModule aus @angular/router importieren. Die Konfiguration erfolgt typischerweise in einer separaten Routing-Modul-Datei:

// app-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HomeComponent } from './home/home.component';
import { AboutComponent } from './about/about.component';
import { ProductListComponent } from './products/product-list.component';
import { ProductDetailComponent } from './products/product-detail.component';
import { PageNotFoundComponent } from './page-not-found/page-not-found.component';

const routes: Routes = [
    { path: 'home', component: HomeComponent },
    { path: 'about', component: AboutComponent },
    { path: 'products', component: ProductListComponent },
    { path: 'products/:id', component: ProductDetailComponent },
    { path: '', redirectTo: '/home', pathMatch: 'full' },
    { path: '**', component: PageNotFoundComponent }
];

@NgModule({
    imports: [RouterModule.forRoot(routes)],
    exports: [RouterModule]
})
export class AppRoutingModule { }

Dann importieren wir dieses Routing-Modul in unserem Haupt-Modul:

// app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
// ... weitere Importe

@NgModule({
    declarations: [
        AppComponent,
        // ... weitere Komponenten
    ],
    imports: [
        BrowserModule,
        AppRoutingModule,
        // ... weitere Module
    ],
    providers: [],
    bootstrap: [AppComponent]
})
export class AppModule { }

19.1.2 Router-Outlet und Navigation

Um die geladenen Komponenten anzuzeigen, platzieren wir die <router-outlet>-Direktive in unserer Haupt-Komponente:

<!-- app.component.html -->
<nav>
    <ul>
        <li><a routerLink="/home" routerLinkActive="active">Home</a></li>
        <li><a routerLink="/about" routerLinkActive="active">About</a></li>
        <li><a routerLink="/products" routerLinkActive="active">Products</a></li>
    </ul>
</nav>

<router-outlet></router-outlet>

Für die programmatische Navigation können wir den Router-Service verwenden:

import { Router } from '@angular/router';

@Component({
    // ...
})
export class MyComponent {
    constructor(private router: Router) {}

    navigateToProduct(productId: number): void {
        this.router.navigate(['/products', productId]);

        // Alternativ mit Parametern:
        this.router.navigate(['/products'], {
            queryParams: { category: 'electronics' }
        });
    }
}

19.2 Fortgeschrittene Routing-Funktionen

19.2.1 Route-Parameter

Route-Parameter sind essentiell für dynamische Inhalte:

// product-detail.component.ts
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, ParamMap } from '@angular/router';
import { switchMap } from 'rxjs/operators';
import { ProductService } from './product.service';
import { Product } from './product.model';
import { Observable } from 'rxjs';

@Component({
    selector: 'app-product-detail',
    templateUrl: './product-detail.component.html'
})
export class ProductDetailComponent implements OnInit {
    product$: Observable<Product>;

    constructor(
        private route: ActivatedRoute,
        private productService: ProductService
    ) {}

    ngOnInit(): void {
        // Reaktiver Ansatz mit Observables
        this.product$ = this.route.paramMap.pipe(
            switchMap((params: ParamMap) => {
                const id = +params.get('id');
                return this.productService.getProduct(id);
            })
        );

        // Alternativ mit Snapshot (nicht reaktiv):
        // const id = +this.route.snapshot.paramMap.get('id');
        // this.productService.getProduct(id).subscribe(product => this.product = product);
    }
}

19.2.2 Query-Parameter

Query-Parameter eignen sich hervorragend für Filterung, Sortierung und Paginierung:

// product-list.component.ts
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';

@Component({
    selector: 'app-product-list',
    template: `
    <div>
      <button (click)="sortBy('name')">Sort by Name</button>
      <button (click)="sortBy('price')">Sort by Price</button>
      <select [ngModel]="currentPage" (ngModelChange)="changePage($event)">
        <option *ngFor="let page of pages" [value]="page">{{page}}</option>
      </select>
    </div>
    <!-- Produktliste -->
  `
})
export class ProductListComponent implements OnInit {
    products: any[] = [];
    currentPage: number = 1;
    pages: number[] = [1, 2, 3, 4, 5];

    constructor(
        private route: ActivatedRoute,
        private router: Router
    ) {}

    ngOnInit(): void {
        // Query-Parameter auslesen
        this.route.queryParamMap.subscribe(params => {
            const sortOrder = params.get('sort') || 'name';
            this.currentPage = +(params.get('page') || 1);

            // Produkte laden und sortieren
            this.loadProducts(sortOrder, this.currentPage);
        });
    }

    sortBy(criteria: string): void {
        this.router.navigate([], {
            relativeTo: this.route,
            queryParams: { sort: criteria, page: this.currentPage },
            queryParamsHandling: 'merge' // behält andere Query-Parameter bei
        });
    }

    changePage(page: number): void {
        this.router.navigate([], {
            relativeTo: this.route,
            queryParams: { page: page },
            queryParamsHandling: 'merge'
        });
    }

    private loadProducts(sortOrder: string, page: number): void {
        // Implementierung der Produkt-Ladung und Sortierung
    }
}

19.2.3 Route Guards

Route Guards schützen Routen und kontrollieren den Zugriff. Sie sind seit den frühen Angular-Versionen verfügbar und wurden in Angular 15+ überarbeitet:

19.2.3.1 Funktionale Route Guards (Angular 15+)

// auth.guard.ts
import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { AuthService } from './auth.service';

export const authGuard: CanActivateFn = (route, state) => {
    const authService = inject(AuthService);
    const router = inject(Router);

    if (authService.isLoggedIn()) {
        return true;
    }

    // Benutzer zur Login-Seite umleiten
    return router.parseUrl('/login?returnUrl=' + state.url);
};

Anwendung in der Route-Konfiguration:

const routes: Routes = [
    {
        path: 'admin',
        component: AdminComponent,
        canActivate: [authGuard]
    }
];

19.2.3.2 Klassenbasierte Guards (vor Angular 15)

// auth.guard.ts
import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router';
import { AuthService } from './auth.service';

@Injectable({
    providedIn: 'root'
})
export class AuthGuard implements CanActivate {
    constructor(private authService: AuthService, private router: Router) {}

    canActivate(
        route: ActivatedRouteSnapshot,
        state: RouterStateSnapshot
    ): boolean {
        if (this.authService.isLoggedIn()) {
            return true;
        }

        this.router.navigate(['/login'], { queryParams: { returnUrl: state.url } });
        return false;
    }
}

Weitere wichtige Guards:

19.2.4 Lazy Loading

Lazy Loading ist eine Technik, die die initiale Ladezeit der Anwendung verringert, indem Module erst dann geladen werden, wenn sie benötigt werden:

// app-routing.module.ts
const routes: Routes = [
    { path: 'home', component: HomeComponent },
    {
        path: 'admin',
        loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule),
        canLoad: [authGuard]
    },
    {
        path: 'products',
        loadChildren: () => import('./products/products.module').then(m => m.ProductsModule)
    }
];

Im Feature-Modul definieren wir dann eigene Routen:

// products/products-routing.module.ts
const routes: Routes = [
    { path: '', component: ProductListComponent },
    { path: ':id', component: ProductDetailComponent }
];

@NgModule({
    imports: [RouterModule.forChild(routes)],
    exports: [RouterModule]
})
export class ProductsRoutingModule { }

19.2.5 Preloading-Strategien

Angular bietet verschiedene Strategien, um Lazy-Loading-Module im Hintergrund vorab zu laden:

// app-routing.module.ts
import { PreloadAllModules } from '@angular/router';

@NgModule({
    imports: [
        RouterModule.forRoot(routes, {
            preloadingStrategy: PreloadAllModules
        })
    ],
    exports: [RouterModule]
})
export class AppRoutingModule { }

Benutzerdefinierte Preloading-Strategien können auch implementiert werden:

// custom-preloading.strategy.ts
import { Injectable } from '@angular/core';
import { PreloadingStrategy, Route } from '@angular/router';
import { Observable, of } from 'rxjs';

@Injectable({
    providedIn: 'root'
})
export class SelectivePreloadingStrategy implements PreloadingStrategy {
    preload(route: Route, load: () => Observable<any>): Observable<any> {
        return route.data && route.data['preload'] ? load() : of(null);
    }
}

Anwendung:

const routes: Routes = [
    {
        path: 'products',
        loadChildren: () => import('./products/products.module').then(m => m.ProductsModule),
        data: { preload: true }
    }
];

@NgModule({
    imports: [
        RouterModule.forRoot(routes, {
            preloadingStrategy: SelectivePreloadingStrategy
        })
    ],
    // ...
})
export class AppRoutingModule { }

19.3 Neuere Routing-Funktionen (Angular 15-17)

19.3.1 Standalone Components & Routing

Seit Angular 14+ wird die Verwendung von Standalone-Komponenten empfohlen. Dies vereinfacht das Routing wesentlich:

// main.ts (Angular 16+)
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { provideRouter, Routes } from '@angular/router';

const routes: Routes = [
  {
    path: 'home',
    loadComponent: () => import('./app/home/home.component').then(c => c.HomeComponent)
  },
  {
    path: 'products',
    loadChildren: () => import('./app/products/routes').then(r => r.PRODUCT_ROUTES)
  }
];

bootstrapApplication(AppComponent, {
  providers: [
    provideRouter(routes)
  ]
});

Für Feature-Routes mit Standalone-Komponenten:

// products/routes.ts
import { Routes } from '@angular/router';

export const PRODUCT_ROUTES: Routes = [
  {
    path: '',
    loadComponent: () => import('./product-list/product-list.component')
      .then(c => c.ProductListComponent)
  },
  {
    path: ':id',
    loadComponent: () => import('./product-detail/product-detail.component')
      .then(c => c.ProductDetailComponent)
  }
];

19.3.2 Routed Komponenten mit Signals (Angular 17+)

Mit Angular 17+ können wir Signals für reaktives Routing nutzen:

// product-detail.component.ts
import { Component, inject, computed } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { ActivatedRoute } from '@angular/router';
import { switchMap, map } from 'rxjs/operators';
import { ProductService } from './product.service';

@Component({
  selector: 'app-product-detail',
  template: `
    <div *ngIf="product(); else loading">
      <h2>{{ product()?.name }}</h2>
      <p>{{ product()?.description }}</p>
      <p>Price: {{ product()?.price | currency }}</p>
    </div>
    <ng-template #loading>Loading...</ng-template>
  `,
  standalone: true
})
export class ProductDetailComponent {
  private route = inject(ActivatedRoute);
  private productService = inject(ProductService);

  private productId = toSignal(
    this.route.paramMap.pipe(
      map(params => Number(params.get('id')))
    )
  );

  product = toSignal(
    computed(() => this.productId()).pipe(
      switchMap(id => this.productService.getProduct(id))
    )
  );
}

19.3.3 Typed Routes (Angular 16+)

Mit Angular 16+ wurden typisierte Routen eingeführt, die viele Vorteile bieten:

// routes.ts
import { Routes } from '@angular/router';

export interface ProductRouteParams {
  id: string;
}

export interface ProductQueryParams {
  category?: string;
  sort?: 'price' | 'name';
}

export const routes = [
  {
    path: 'products',
    children: [
      { 
        path: '', 
        component: ProductListComponent,
      },
      { 
        path: ':id', 
        component: ProductDetailComponent,
      }
    ]
  }
] as const satisfies Routes;

Mit dem @angular/router Package können wir nun die Routen typisieren:

// navigation.service.ts
import { inject, Injectable } from '@angular/core';
import { Router, convertRouteState } from '@angular/router';
import { routes, ProductRouteParams, ProductQueryParams } from './routes';

@Injectable({
  providedIn: 'root'
})
export class NavigationService {
  private router = inject(Router);
  private typedRouter = convertRouteState(routes);

  navigateToProductDetail(id: number, options?: { queryParams?: ProductQueryParams }): void {
    // Typsicherheit für Route-Parameter und Query-Parameter
    this.typedRouter.navigate(['products', id.toString()], {
      queryParams: options?.queryParams
    });
  }

  navigateToProductList(queryParams?: ProductQueryParams): void {
    this.typedRouter.navigate(['products'], {
      queryParams
    });
  }
}

19.3.4 Hash-basiertes vs. HTML5 Routing

Angular unterstützt sowohl Hash-basiertes als auch HTML5-Routing:

// HTML5 Routing (Standard)
@NgModule({
  imports: [RouterModule.forRoot(routes, { useHash: false })],
  exports: [RouterModule]
})
export class AppRoutingModule { }

// Hash-basiertes Routing
@NgModule({
  imports: [RouterModule.forRoot(routes, { useHash: true })],
  exports: [RouterModule]
})
export class AppRoutingModule { }

Mit Hash-Routing würden URLs wie https://example.com/#/products statt https://example.com/products aussehen.

Für Standalone-Komponenten (Angular 16+):

bootstrapApplication(AppComponent, {
  providers: [
    provideRouter(routes, withHashLocation())
  ]
});

19.3.5 Relative Navigation und Route Reuse

Relative Navigation ist besonders nützlich für verschachtelte Routen:

// Bei aktueller Route /products/123
this.router.navigate(['specifications'], { relativeTo: this.route });
// Navigiert zu /products/123/specifications

// Eine Ebene höher navigieren
this.router.navigate(['../456'], { relativeTo: this.route });
// Navigiert zu /products/456

Für verbesserte Performance können wir die RouteReuseStrategy anpassen:

// custom-route-reuse-strategy.ts
import { Injectable } from '@angular/core';
import { RouteReuseStrategy, ActivatedRouteSnapshot, DetachedRouteHandle } from '@angular/router';

@Injectable()
export class CustomRouteReuseStrategy implements RouteReuseStrategy {
  private handlers: Map<string, DetachedRouteHandle> = new Map();

  shouldDetach(route: ActivatedRouteSnapshot): boolean {
    return route.routeConfig?.path === 'products';
  }

  store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle): void {
    if (route.routeConfig?.path) {
      this.handlers.set(route.routeConfig.path, handle);
    }
  }

  shouldAttach(route: ActivatedRouteSnapshot): boolean {
    return !!route.routeConfig?.path && !!this.handlers.get(route.routeConfig.path);
  }

  retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle | null {
    if (!route.routeConfig?.path) return null;
    return this.handlers.get(route.routeConfig.path) || null;
  }

  shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean {
    return future.routeConfig === curr.routeConfig;
  }
}

Und im Provider verwenden:

@NgModule({
  // ...
  providers: [
    { provide: RouteReuseStrategy, useClass: CustomRouteReuseStrategy }
  ]
})
export class AppModule { }

19.3.6 Router Events

Mit Router Events können wir auf den Navigationszyklus reagieren:

import { Router, NavigationStart, NavigationEnd, NavigationCancel, NavigationError } from '@angular/router';
import { filter } from 'rxjs/operators';

@Component({
  // ...
})
export class AppComponent implements OnInit {
  loading = false;

  constructor(private router: Router) {}

  ngOnInit() {
    this.router.events.pipe(
      filter(event => 
        event instanceof NavigationStart ||
        event instanceof NavigationEnd ||
        event instanceof NavigationCancel ||
        event instanceof NavigationError
      )
    ).subscribe(event => {
      if (event instanceof NavigationStart) {
        this.loading = true;
      } else {
        this.loading = false;
        
        if (event instanceof NavigationEnd) {
          // Analytik-Code hier
          console.log('Navigation erfolgreich zu:', event.url);
        }
        
        if (event instanceof NavigationError) {
          console.error('Navigationsfehler:', event.error);
        }
      }
    });
  }
}

19.3.7 Deferred Loading (Angular 17+)

Angular 17 führte das @defer-Feature ein, das das teilweise Laden von Templates ermöglicht:

<!-- product-detail.component.html -->
<div>
  <h1>{{product.name}}</h1>
  <p>{{product.description}}</p>
  
  @defer (on viewport) {
    <app-product-reviews [productId]="product.id"></app-product-reviews>
  } @loading {
    <p>Lade Bewertungen...</p>
  }
  
  @defer (when isLoggedIn()) {
    <app-product-edit-button [product]="product"></app-product-edit-button>
  }
</div>

Dieses Feature arbeitet nahtlos mit dem Router zusammen und verbessert die Performance, besonders bei komplexen Views.

19.4 Best Practices für Angular Routing (15+)

  1. Route-Definitionen strukturieren: Separate Routing-Module für jedes Feature-Modul erstellen
  2. Lazy Loading verwenden: Besonders für größere Anwendungen
  3. Typed Routes nutzen: Für bessere Typsicherheit und Entwicklererfahrung
  4. Route-Guards für Sicherheit einsetzen: Besonders für geschützte Bereiche
  5. Data-Property für Meta-Daten verwenden: Titel, Beschreibungen oder Berechtigungen
  6. Resolve für Datenvorab-Laden nutzen: Verbessert die Benutzererfahrung
  7. RouteReuseStrategy anpassen: Für komplexe Anwendungen mit wiederverwendbaren Views
  8. Signals für reaktives Routing nutzen: In neueren Angular-Versionen (17+)

Das Routing-System in Angular hat sich über die Jahre stetig weiterentwickelt und bietet heute eine leistungsfähige und flexible Lösung für die Navigation in modernen Webanwendungen. Mit den neuesten Features wie Standalone Components, Typed Routes und Signal-Integration ist es noch einfacher geworden, komplexe Navigationsstrukturen zu implementieren und dabei eine optimale Performance zu gewährleisten.

Die Entwicklung von Angular 15 bis heute zeigt einen klaren Trend zu mehr Typsicherheit, besserer Performance durch präziseres Lazy-Loading und einer vereinfachten API durch funktionale Patterns und Standalone-Komponenten.