Routing in Angular Web Komponenten

Der Angular Router ist ein sehr leistungsfähiges Werkzeug und einfach in einem regulären Angular-Projekt einzurichten. Aber wie kann man seine Stärken in einer Angular Web Komponente nutzen?

Last updated on 25. Oktober 2020
By Timon Grassl

Einführung

Stelle dir sich zunächst die folgende Anwendungsstruktur vor:

RootApplication
├── ComponentA
├── ComponentB
├── WebComponents
    ├── ElementA
    │    └── SettingsHome
    │    └── SettingsProfile
    └── ElementB
         └── ElementBComponent

Unsere index.html sieht in etwa so aus:

<app-root modules-to-load="element-a, element-b"></app-root>
<element-a></element-a>
<element-b></element-b>

Unsere Root-Anwendung ist dafür verantwortlich, dass unsere Webkomponenten langsam geladen und die Element-Tags der Webkomponenten bei Bedarf gefüllt werden. Sie implementiert auch das RoutingModul zum Routen zu ComponentA oder ComponentB.

In jeder Web-Komponente wollen wir jetzt auch den Angular Router verwenden, aber da es sich um eine gekapselte Angular Module handelt, wird die Sache hier etwas knifflig.

Kann ich nicht einfach RouterModule.forRoot() verwenden?

Da unsere Root-Anwendung bereits das RouterModule forRoot Angular verwendet, tritt ein Fehler auf.

Was ist mit RouterModule.forChild()?

Außerdem wird es nicht funktionieren, da unsere Webkomponenten wie kleine eigenständige Angular Applikationen sind und nicht wissen, ob es ein Root-RouterModule in einer anderen Angular App gibt.

Kann ich den Angular Router überhaupt in meinen Webkomponenten verwenden?

Ja und nein.

Da der Router den Pfad unserer URL modifiziert, müssten wir Routenänderungen von mehreren Outlets über die gesamte Seite verarbeiten.

Nehmen wir an, wir würden auf unserer Seite https://example.com zu ComponentA routen, indem wir zum Pfad /component-a navigieren. Die URL sieht nun wie folgt aus: https://example.com/component-a

Was aber, wenn wir gleichzeitig auch zu einer Komponente innerhalb einer Web-Komponente, z.B. ElementA, routen wollen?

Im Moment denke ich, die beste Lösung dafür wäre, named router outlets innerhalb der Web-Komponente zu verwenden und sie an den aktuellen Pfad anzuhängen. Bei dieser Strategie kann die URL sowohl die Hauptroute als auch unsere sekundären Routen enthalten: https://example.com/component-a(elementA:settingsHome) (Mehr dazu später).

Die zweite Möglichkeit wäre, das Routing zu verwenden, ohne die Routenänderungen überhaupt in der Browser-URL anzuzeigen. Angular wird bereits mit einer großartigen Lösung hierfür ausgeliefert - der MockLocationStrategy.

MockLocationStrategy wird technisch nur zu Testzwecken eingesetzt, aber da sie simulierte Location-Events abfeuert, ist sie auch eine großartige Lösung für unser Routing-Problem. Das heißt, wenn wir zu SettingsHome innerhalb von ElementA navigieren, wird der Browser-Pfad nicht geändert und liest immer noch die vorherige URL. https://example.com/component-a

Einbindung

Richte zuerst deine Routen innerhalb der Web-Komponente ein, z.B. ElementA und definiere auch ein named router outlet innerhalb der Root-Komponente von ElementA.

element-a-routing.module.ts

const routes = [
      { path: '', component: SettingsHomeComponent, outlet: 'elementA' },
      { path: 'profile', component: SettingsProfileComponent, outlet: 'elementA' },
      { path: '**', redirectTo: '', pathMatch: 'full'}
    ]

element-a.component.html

<router-outlet name="elementA"></router-outlet>

Füge dann das RouterTestingModule (es enthält bereits die MockLocationStrategy) zu den Importen des ElementARoutingModule hinzu und rufe withRoutes(routes) auf, um die Routen einzurichten.

import { RouterTestingModule } from '@angular/router/testing';
import { NgModule } from '@angular/core';
import { SettingsHomeComponent } from './components/settings-home/settings-home.component';
import { SettingsProfileComponent } from './components/settings-profile/settings-profile.component';

const routes = [
      { path: '', component: SettingsHomeComponent, outlet: 'elementA' },
      { path: 'profile', component: SettingsProfileComponent, outlet: 'elementA' },
      { path: '**', redirectTo: '', pathMatch: 'full'}
    ];

@NgModule({
  imports: [
    RouterTestingModule.withRoutes(routes),
  ],
  exports: [RouterTestingModule],
})
export class ElementARoutingModule {}

Danach muss das ElementARoutingModul in das ElementAModul importiert werden.

import { NgModule, Injector } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ElementAComponent } from './element-a.component';
import { createCustomElement } from '@angular/elements';
import { SettingsHomeComponent } from './components/settings-home/settings-home.component';
import { SettingsProfileComponent } from './components/settings-picture/settings-picture.component';
import { ElementARoutingModule } from './element-a-routing.module';

@NgModule({
  declarations: [ElementAComponent, SettingsHomeComponent, SettingsProfileComponent],
  imports: [
    CommonModule,
    ElementARoutingModule
  ],
  entryComponents: [ElementAComponent]
})
export class ElementAModule {

  constructor(private injector: Injector) {
    const elementA = createCustomElement(ElementAComponent, { injector });
    customElements.define('element-a', elementA);
  }

  ngDoBootstrap() {}
}

Rufe schließlich initialNavigation() innerhalb der ElementAComponent auf, und schon kann es losgehen!

import { Component, OnInit, Input } from '@angular/core';
import { Router } from '@angular/router';

@Component({
  selector: 'element-a',
  templateUrl: './element-a.component.html',
  styleUrls: ['./element-a.component.scss']
})
export class ElementAComponent implements OnInit {

  constructor(
    private router: Router) {
  }

  ngOnInit() {
    this.router.initialNavigation(); // Manually triggering initial navigation for @angular/elements
  }

}

Jetzt kann man in ElementA zu den Routen innerhalb der Komponenten navigieren! Mit router.navigate:

this.router.navigate([{outlets: {elementA: 'ROUTE'}}]);

oder über die [routerLink] Direktive:

<button [routerLink]="[{outlets: {elementA: 'ROUTE'}}]"> Zu den Einstellungen</button>

Um zur leeren Route zu navigieren, verwende null anstelle des Namens der Route.

Mehr Infos

Noch mehr Informationen über das Thema erhältst du auf Medium oder auf GitHub

Timon Grassl
Software Engineer
Timon Grassl
Software Engineer