Angular Tour of heroes als Standalone-App

Das populäre Angular-Tutorial Tour of Heroes funktioniert zur Zeit nicht, weil das Angular-Kommandozeilentool mittlerweile standardmäßig Standalone-Applikationen erzeugt. Man kann diese Probleme umgehen, indem man eine klassische nicht-Standalone-Applikation erzeugt. Es ist aber auch sehr lehrreich, einfach bei der Standalone-Version zu bleiben und die Probleme selbst zu fixen.

Table Of Contents

Was ist der Standalone-Modus?

In der Vergangenheit waren Angular-Applikationen in Module strukturiert, die zur Verdrahtung der verschiedenen Komponenten und Module untereinander dienten. Das brachte allerdings einiges an Ballast mit. Beginnend mit Angular 14, kann dieser Ballast durch Standalone-Komponenten vermieden werden. Diese sind schlanker und auch flexibler zu verwenden.

Damit einher geht eine Aufwertung des Bootstrapping-Codes im Einstiegspunkt der Applikation in src/main.ts. Dieser übernimmt nunmehr Aufgaben bei der Injizierung von Abhängigkeiten, die früher von Angular-Modulen übernommen worden sind.

Mehr Informationen finden man in der Angular-Dokumentation im Artikel Getting started with standalone components.

Leider wurde aber das populäre Angular-Tutorial "Tour of Heroes" noch nicht angepasst. Dieser Blog-Eintrag springt hier ein und zeigt, wie die auftretenden Fehler behoben werden können, wenn die Angular-Applikation mit Standalone-Komponenten erzeugt wurde. Das ist zur Zeit (Angular 17) der Default.

Die fertige Applikation kann man im begleitenden Git-Repository standalone-angular-tour-of-heroes betrachten. Die Commits in das Repository folgen im Großen und Ganzen den Schritten des Tutorials.

Unterschiede zu den Tutorial-Anweisungen

Dieser Blog-Post hat nicht den Anspruch das Tutorial neu zu schreiben, sondern hebt lediglich die Maßnahmen hervor, die notwendig sind, um den Code mit Angular 17 zum Laufen zu bekommen.

Ein Projekt erzeugen

Es ist normalerweise eine gute Idee, Angular-Applikation im strikten Modus zu erzeugen, weil dies hilft, Fehler im TypeScript-Code zu vermeiden.

$ npx ng new --strict standalone-angular-tour-of-heroes

Als Stylesheet-Format sollte CSS ausgewählt werden. Das Server-Side-Rendering wird mit "No" deaktiviert, weil dieses Feature für das Tutorial nicht notwendig ist. Ergebnis ist eine Angular-Applikation ohne die Datei src/app/app.module.ts. Wann immer im Tutorial die Rede von dieser Datei ist, bedeutet das demnach, dass etwas geändert werden muss.

Für den Augenblick kann man aber dem Original-Tutorial "Tour of Heroes" folgen, bis der erste Fehler auftritt.

Der Helden-Editor

Generierung der Komponente HeroesComponent

Die Erzeugung der Komponente HeroesComponent funktioniert zwar. Aber wenn sie ins Template src/app/app.component.html eingefügt wird, kommt es zum ersten Fehler.

✘ [ERROR] NG8001: 'app-heroes' is not a known element:
1. If 'app-heroes' is an Angular component, then verify that it is included in the '@Component.imports' of this component.
2. If 'app-heroes' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@Component.schemas' of this component to suppress this message. [plugin angular-compiler]

    src/app/app.component.html:2:0:
      2 │ <app-heroes></app-heroes>
        ╵ ~~~~~~~~~~~~

  Error occurs in the template of component AppComponent.

    src/app/app.component.ts:8:14:
      8 │   templateUrl: './app.component.html',
        ╵                ~~~~~~~~~~~~~~~~~~~~~~

In der Vergangenheit aktualisierte das Kommando ng generate component automatisch das Modul, dass die Komponenten innerhalb desselben miteinander verdrahtete. In Standalone-Modus sind alle Komponenten individuell nutzbar und voneinander unabhängig, und man ist selber für den Import in andere Komponenten verantwortlich. Dazu machen wir die folgenden Änderungen an src/app/app.component.ts:

import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { HeroesComponent } from './heroes/heroes.component';

@Component({
    selector: 'app-root',
    standalone: true,
    imports: [RouterOutlet, HeroesComponent],
    templateUrl: './app.component.html',
    styleUrl: './app.component.css',
})
export class AppComponent {
    title = 'Standalone Tour of Heroes';
}

Es ist entscheiden, @Component.standalone auf true zu setzen. Andernfalls gibt es einen Fehler:

[ERROR] TS-992010: 'imports' is only valid on a component that is standalone. [plugin angular-compiler]

Das bedeutet, dass man nur in Standalone-Komponenten direkt andere Module und Komponenten importieren kann.

Das obige Listing zeigt die komplette Quelldatei. Von jetzt an werde ich allerdings lediglich die Änderungen an der eigentlichen Implementierung zeigen. Die notwendigen import-Statements im Kopf der Datei müssen selbst zugefügt werden. Wer eine IDE verwendet, profitiert davon, dass dies mehr oder weniger automatisch passiert.

Wird zum Beispiel HeroesComponent zum Array imports zugefügt, zeigt die populäre Entwicklungsumgebung Visual Studio Code einen Fehler an, indem das Symbol mit Tilden (~~~~~) unterstrichen wird. Grund ist, dass es nicht von src/app/heroes/heroes.component.ts importiert wurde. Fährt man mit der Maus ˝über die Fehlerstelle, wird eine detaillierte Problembeschreibung angezeigt.

Problembeschreibung in Visual Studio Code

Daraus geht hervor, dass die Komponente HeroesComponent nicht gefunden werden kann. Die gute Nachricht ist, dass ein Klick auf Quick fix Vorschläge zur Behebung des Problems anzeigt.

Vorschläge zur Fehlerbehebung von Visual Studio Code

Der Vorschlag, einen Import aus ./heroes/heroes.component zuzufügen, ist korrekt. Wird er ausgewählt, fügt Visual Studio Code das fehlende import-Statement im Kopf der Datei zu.

Die Applikation sollte jetzt kompilieren und wie erwartet funktionieren.

Formatierung mit der Pipe uppercase

Wird die Pipe uppercase ins Template für HeroesComponent eingefügt, knallt es das nächste Mal:

✘ [ERROR] NG8004: No pipe found with name 'uppercase'. [plugin angular-compiler]

    src/app/heroes/heroes.component.html:1:19:
      1 │ <h2>{{ hero.name | uppercase }} Details</h2>
        ╵                    ~~~~~~~~~

  Error occurs in the template of component HeroesComponent.

    src/app/heroes/heroes.component.ts:8:14:
      8 │   templateUrl: './heroes.component.html',
        ╵                ~~~~~~~~~~~~~~~~~~~~~~~~~

Das nunmehr fehlende AppModule importiert normalerweise das CommonModule aus @angular/common. Das muss man jetzt selber tun, und zwar in src/app/app.component.ts. Dazu muss der Aufruf der Decorator-Funktion @Component wie folgt geändert werden:

@Component({
    selector: 'app-heroes',
    standalone: true,
    imports: [CommonModule],
    templateUrl: './heroes.component.html',
    styleUrl: './heroes.component.css',
})

Nicht vergessen, @Component.standalone auf true zu setzen, und oben in der Datei CommonModule zu importieren!

Helden editieren

Es gibt einen weiteren Fehler:

✘ [ERROR] NG8002: Can't bind to 'ngModel' since it isn't a known property of 'input'. [plugin angular-compiler]

Jetzt fehlt das FormsModule, das allerdings nicht ins AppModule importiert werden kann, weil es kein AppModule gibt. Es muss in src/app/heroes/heroes.component.ts importiert werden, genau wie das CommonModule vorher:

@Component({
    selector: 'app-heroes',
    standalone: true,
    imports: [CommonModule, FormsModule],
    templateUrl: './heroes.component.html',
    styleUrl: './heroes.component.css',
})

Nicht vergessen, @Component.standalone auf true zu setzen!

Feature-Komponente erzeugen

Das Template für HeroesComponent anpassen

Wird das Template für HeroesComponent angepasst, so dass es die KomponentenHeroDetailComponent anzeigt, führt das zum nächsten Fehler:

[ERROR] NG8001: 'app-hero-detail' is not a known element:
...

Das Problem sollte jetzt klar sein. Die Komponente HeroDetailComponent muss von HeroesComponent importiert werden:

@Component({
    selector: 'app-heroes',
    standalone: true,
    imports: [CommonModule, FormsModule],
    templateUrl: './heroes.component.html',
    styleUrl: './heroes.component.css',
})

Es gibt aber noch mehr Fehler und Warnungen:

...
▲ [WARNING] NG8103: The `*ngIf` directive was used in the template, but neither the `NgIf` directive nor the `CommonModule` was imported. Use Angular's built-in control flow @if or make sure that either the `NgIf` directive or the `CommonModule` is included in the `@Component.imports` array of this component. [plugin angular-compiler]
...
✘ [ERROR] NG8004: No pipe found with name 'uppercase'. [plugin angular-compiler]
...
✘ [ERROR] NG8002: Can't bind to 'ngModel' since it isn't a known property of 'input'. [plugin angular-compiler]
...

Wir benutzen die Direktive ngIf ohne sie importiert zu haben. Vorher fehlte schon die Pipe uppercase, und ngModel ist ebenfalls nicht bekannt. Dies muss in src/app/hero-detail.component.ts gefixt werden, indem ein paar Symbole importiert werden:

@Component({
    selector: 'app-hero-detail',
    standalone: true,
    imports: [CommonModule, FormsModule, NgIf],
    templateUrl: './hero-detail.component.html',
    styleUrl: './hero-detail.component.css',
})

Services

HeroComponent anpassen

Bei der Deklaration der Eigenschaft heroes der HeroComponent, meckert der TypeScript-Compiler einmal mehr:

✘ [ERROR] TS2564: Property 'heroes' has no initializer and is not definitely assigned in the constructor. [plugin angular-compiler]

Das passiert, weil wir bei der Generierung der Applikation den strikten Modus aktiviert haben. Genauer gesagt, liegt es an den Einstellungen in tsconfig.json im Wurzelverzeichnis der Applikation. Beheben lässt sich das mit einem einzigen zusätzlichen Fragezeichen in src/app/heroes/heroes.component.ts:

export class HeroesComponent {
    heroes?: Hero[];
    selectedHero?: Hero;
    // ...
}

Damit ist auch heroes als optional markiert. Für selectedHero war das bereits vorher geschehen.

Jetzt sollte alles wieder funktionieren.

Benachrichtigungs-Komponente erzeugen

Wird die Benachrichtigungs-Komponente dem Applikations-Template src/app/app.component.html zugefügt, kommt der nächste Fehler:

✘ [ERROR] NG8001: 'app-messages' is not a known element:

Das kennen wir mittlerweile zur Genüge und wir wissen, was zu tun ist. Wir müssen die Importe der AppComponent in src/app/app.component.ts erweitern:

@Component({
    selector: 'app-root',
    standalone: true,
    imports: [RouterOutlet, HeroesComponent, MessagesComponent],
    templateUrl: './app.component.html',
    styleUrl: './app.component.css',
})

MessageService anbinden

Die MessagesComponent verwendet die Direktiven *NgIf und *NgFor. Das ruft weitere Warnungen hervor:

▲ [WARNING] NG8103: The `*ngIf` directive was used in the template, but neither the `NgIf` directive nor the `CommonModule` was imported. Use Angular's built-in control flow @if or make sure that either the `NgIf` directive or the `CommonModule` is included in the `@Component.imports` array of this component. [plugin angular-compiler]
...
▲ [WARNING] NG8103: The `*ngFor` directive was used in the template, but neither the `NgFor` directive nor the `CommonModule` was imported. Use Angular's built-in control flow @for or make sure that either the `NgFor` directive or the `CommonModule` is included in the `@Component.imports` array of this component. [plugin angular-compiler]

Um die Warnungen loszuwerden, müssen die Direktiven in src/app/messages/messages.component.ts importiert werden:

@Component({
    selector: 'app-messages',
    standalone: true,
    imports: [NgFor, NgIf],
    templateUrl: './messages.component.html',
    styleUrl: './messages.component.css',
})

Importiert werden sie oben in der Datei aus @angular/common.

Navigation mit Routing

Einer der großen Vorteile des Standalone-Modus ist die Art und Weise wie Routing implementiert wird. Das ist jetzt erheblich einfacher und klarer.

Das AppRoutingModule erzeugen

Besser gesagt: Das Modul sollte nicht erzeugt werden, weil es nicht mehr benötigt wird. Der entsprechende Schritt im Tutorial sollte daher komplett übersprungen werden zugunsten der Anleitung, die hier gegeben wird.

Schauen wir uns aber zuerst den Haupteinstiegspunkt der Applikation src/main.ts an. Der sollte so aussehen:

import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { AppComponent } from './app/app.component';

bootstrapApplication(AppComponent, appConfig).catch(err => console.error(err));

Wie man sieht, wird die Funktion bootstrapApplication() mit zwei Argumenten aufgerufen. Das erste ist eine Komponente und das zweite ein optionales Objekt vom Typ ApplicationConfig.

Die Konfiguration wurde aber in eine separate Datei src/app/app.config.ts ausgelagert:

import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';

import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
    providers: [provideRouter(routes)],
};

Ohne Standalone-Modus, stellen Module andere Module wie RouterModule zur Verfügung. Weil die Modulebene aber entfernt wurde, wird diese Aufgabe vom Boostrapping-Code der Applikation übernommen. Angular stellt dafür fertige Provider zur Verfügung, deren Namen der Konvention provide*IRGENDETWAS* folgen. Im vorliegenden Fall ist das provideRouter.

Vergleichen wir das mit dem AppRoutingModule im Original-Tutorial:

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HeroesComponent } from './heroes/heroes.component';

const routes: Routes = [
  { path: 'heroes', component: HeroesComponent }
];

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

Diese Modul importiert das RouterModule für die Wurzel der Applikation (forRoot()) mit den Routen als Argument. Das wird jetzt mit einem einzigen Aufruf von provideRouter ersetzt, wiederum mit einem Array vom Typ Routes als Argument.

Und um alles schön strukturiert und sauber getrennt zu halten, werden die Routen routes wiederum aus einer separaten Datei src/app/app.routes.ts importiert:

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

export const routes: Routes = [];

Und genau hier müssen jetzt die Routen definiert werden. Die erste Route fügen wir folgendermaßen in src/app/app.routes.ts zu:

import { Routes } from '@angular/router';
import { HeroesComponent } from './heroes/heroes.component';

export const routes: Routes = [
    { path: 'heroes', component: HeroesComponent },
];

Jetzt kann <router-outlet></router-outlet> zum Template der Applikationskomponente zugefügt werden, genau wie im Tutorial beschrieben. Ruft man im Browser http://localhost:3000/heroes auf, sollte die Route verfolgt und die Helden-Komponente angezeigt werden.

Ich habe aber schon das AppRoutingModule eingebaut ...

Was aber, wenn man der Anleitung im Tutorial gefolgt ist? Welche Fehler treten dann auf?

Wer sich an die Anweisung in diesem Beitrag gehalten hat, kann gerne auch direkt zum nächsten Abschnitt Mit routerLink einen Navigations-Link zufügen Dieser Abschnitt ist hauptsächlich für Leute gedacht, die nach den Fehlermeldungen googeln und eine Lösung suchen.

Schon die Generierung des AppRoutingModule, wie sie im Tutorial beschrieben wird, schlägt fehl:

$ ng generate module app-routing --flat --module=app
Specified module 'app' does not exist.
Looked in the following directories:
    /src/app/app-routing
    /src/app/app
    /src/app
    /src

Aus der Fehlermeldung lässt sich schließen, dass man die Option --module=app weglassen sollte, weil wir Standalone-Komponenten und keine Module verwenden. Ohne die Option klappt es tatsächlich:

$ ng generate module app-routing --flat
CREATE src/app/app-routing.module.ts (196 bytes)

Ersetzt man jetzt <app-heroes></app-heroes> mit <router-outlet></router-outlet> in src/app/app.component.html, funktioniert das auf den ersten Blick. Man öffnet http://localhost:4200 im Browser und sieht nur den Titel der Applikation, genau wie im Tutorial beschrieben. Aber was passiert, wenn man http://localhost:4200/heroes in die Adresszeile eingibt? Nichts!

Ein Blick in die JavaScript-Konsole des Browsers offenbart aber den Fehler:

main.ts:5 ERROR Error: NG04002: Cannot match any routes. URL Segment: 'heroes'
    at Recognizer.noMatchError (router.mjs:3687:12)
    at router.mjs:3720:20
    at catchError.js:10:39
    at OperatorSubscriber2._this._error (OperatorSubscriber.js:25:21)
    at Subscriber2.error (Subscriber.js:43:18)
    at Subscriber2._error (Subscriber.js:67:30)
    at Subscriber2.error (Subscriber.js:43:18)
    at Subscriber2._error (Subscriber.js:67:30)
    at Subscriber2.error (Subscriber.js:43:18)
    at Subscriber2._error (Subscriber.js:67:30)

Weshalb passiert das? Theoretisch sollte das AppRoutingModule wie erwartet funktionieren. Das Problem ist allerdings, dass es nirgendwo in der ganzen Applikation verwendet wird. Nicht eine einzige TypeScript-Datei enthält ein import-Statement aus src/app/app-routing.module.ts. Das bedeutet, dass überhaupt keine Routen definiert wurden, und deshalb auch keine passenden gefunden werden können.

Das sollte sich doch eigentlich beheben lassen, indem wir das AppRoutingModule in die Komponente in src/app/app.component.ts importieren, oder? Schauen wir einmal, was passiert.

@Component({
    selector: 'app-root',
    standalone: true,
    // Adding the AppRoutingModule does NOT work!
    imports: [RouterOutlet, HeroesComponent, MessagesComponent, AppRoutingModule],
    templateUrl: './app.component.html',
    styleUrl: './app.component.css',
})

Das passiert: Die JavaScript-Konsole schmeißt neue Fehler:

main.ts:5 ERROR Error: NG04007: The Router was provided more than once. This can happen if 'forRoot' is used outside of the root injector. Lazy loaded modules should use RouterModule.forChild() instead.
    at Object.provideForRootGuard [as useFactory] (router.mjs:7445:11)
    at Object.factory (core.mjs:3322:38)
    at core.mjs:3219:47
    at runInInjectorProfilerContext (core.mjs:866:9)
    at R3Injector.hydrate (core.mjs:3218:21)
    at R3Injector.get (core.mjs:3082:33)
    at injectInjectorOnly (core.mjs:1100:40)
    at ɵɵinject (core.mjs:1106:42)
    at Object.RouterModule_Factory [as useFactory] (router.mjs:7376:41)
    at Object.factory (core.mjs:3322:38)

Die Fehlermeldung empfiehlt, mit RouterModule.forChild() zu importieren. Das probieren wir in src/app/app-routing.module.ts aus:

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

Und tatsächlich ist damit das Problem für die Startseite http://localhost:4200 behoben. Gehen wir allerdings zu http://localhost:4200/heroes, sehen wir wieder die gleiche Fehlermeldung wie gerade eben: "ERROR Error: NG04002: Cannot match any routes. URL Segment: 'heroes'".

Es bleibt nichts anderes übrig, als src/app/app-routing.module.ts zu löschen und der Anleitung im Abschnitt Navigation mit Routing zu folgen, was obendrein auch einfacher ist.

Navigations-Link mit routerLink

Der Link wurde jetzt mit routerLink zum Komponenten-Template von ApplicationComponent zugefügt, aber er lässt sich leider nicht klicken. Und Angular schreibt noch nicht einmal eine Fehlermeldung in die JavaScript-Konsole.

Das Problem ist der Tatsache geschuldet, dass der Template-Schnipsel <a routerLink="/heroes"> absolut legales HTML ist. Lediglich das unbekannte Attribut routerLink wird vom Browser ignoriert

Damit es eine eigene Bedeutung erlangt, muss erst das RouterModule in src/app/app.component.ts importiert werden:

@Component({
    selector: 'app-root',
    standalone: true,
    imports: [RouterOutlet, RouterModule, HeroesComponent, MessagesComponent],
    templateUrl: './app.component.html',
    styleUrl: './app.component.css',
})

Das Symbol RouterModule muss aus @angular/router importiert werden, damit es funktioniert.

Jetzt lässt sich der Link klicken und leitet zur Heldenliste.

Dashboard integrieren

Das Tutorial erklärt, dass die neuen Routen in src/app/app-routing.module.ts zugefügt werden müssen. Wir haben aber kein Modul. Wir fügen sie stattdessen in src/app/app.routes.ts ein. Am Ende sollte das Array routes so aussehen:

export const routes: Routes = [
    { path: '', redirectTo: '/dashboard', pathMatch: 'full' },
    { path: 'heroes', component: HeroesComponent },
    { path: 'dashboard', component: DashboardComponent },
];

Aber das funktioniert noch nicht wie gewünscht. In der JavaScript-Konsole sieht man den Fehler:

dashboard.component.html:3 NG0303: Can't bind to 'ngForOf' since it isn't a known property of 'a' (used in the '_DashboardComponent' component template).
1. If 'a' is an Angular component and it has the 'ngForOf' input, then verify that it is a part of an @NgModule where this component is declared.
2. To allow any property add 'NO_ERRORS_SCHEMA' to the '@NgModule.schemas' of this component.

Wir müssen NgFor aus @angular/common in die Dashboard-Komponente src/app/dashboard/dashboard.component.ts importieren:

@Component({
    selector: 'app-dashboard',
    standalone: true,
    imports: [NgFor],
    templateUrl: './dashboard.component.html',
    styleUrls: ['./dashboard.component.css'],
})

Nicht vergessen, @Component.standalone auf true zu setzen!

Zu den Helden-Details navigieren

Wir haben kein AppRoutingModule, und deshalb müssen die neuen Routen in src/app/app.routes.ts definiert werden:

export const routes: Routes = [
    { path: '', redirectTo: '/dashboard', pathMatch: 'full' },
    { path: 'heroes', component: HeroesComponent },
    { path: 'dashboard', component: DashboardComponent },
    { path: 'detail/:id', component: HeroDetailComponent },
];

Wenn wir jetzt routerLink in src/app/dashboard/dashboard.html einbauen, kommt es zu einem Fehler, den wir bereits kennen:

✘ [ERROR] NG8002: Can't bind to 'routerLink' since it isn't a known property of 'a'. [plugin angular-compiler]

Das bedeutet, dass auch das RouterModule aus src/app/dashboard/dashboard.ts importiert werden muss:

@Component({
    selector: 'app-dashboard',
    standalone: true,
    imports: [NgFor, RouterModule],
    templateUrl: './dashboard.component.html',
    styleUrls: ['./dashboard.component.css'],
})

Den gleichen Fehler gibt es für HeroesComponent. Also fügen wir den Import src/app/heroes/heroes.component.ts zu:

@Component({
    selector: 'app-heroes',
    standalone: true,
    imports: [CommonModule, NgFor, RouterModule],
    templateUrl: './heroes.component.html',
    styleUrl: './heroes.component.css',
})

Den Weg zurück finden

Bei der Implementierung der Methode goBack()in HeroDetailComponent stößt man eventuell auf dieses Problem:

✘ [ERROR] TS2339: Property 'back' does not exist on type 'Location'. [plugin angular-compiler]

In diesem Fall muss überprüft werden, ob folgendes import-Statement vorhanden ist:

import { CommonModule, Location, NgIf } from '@angular/common';

Es ist wichtig, dass Location aus @angular/common importiert wird, weil es auch ein global verfügbares interface mit dem gleichen Namen Location gibt. Das ist, was benutzt wird, um in JavaScript mit document.location.href = 'woanders' einen Redirect zu realisieren. Was wir brauchen, ist aber die Klasse Location aus @angular/common.

Daten von einem Server abrufen

HttpClient importieren

Im Tutorial wird HttpClientModule in AppModule importiert. Aber weil es in unserer Standalone-App kein AppModule gibt, muss es in die Komponenten, die es verwenden, importiert werden. Wir ignorieren das also für den Augenblick genauso wie die anderen Modifikationen am AppModule, nach denen gefragt wurde.

Nachdem aber die anderen Komponenten wie im Tutorial beschrieben geändert wurden, ist es Zeit den Import von HttpClientModule zu fixen, weil es einen Fehler in der JavaScript-Konsole gibt:

main.ts:5 ERROR NullInjectorError: R3InjectorError(Standalone[_DashboardComponent])[_HeroService -> _HeroService -> _HeroService -> _HttpClient -> _HttpClient]: 
  NullInjectorError: No provider for _HttpClient!
    at NullInjector.get (core.mjs:1654:27)
    at R3Injector.get (core.mjs:3093:33)
    at R3Injector.get (core.mjs:3093:33)
    at injectInjectorOnly (core.mjs:1100:40)
    at Module.ɵɵinject (core.mjs:1106:42)
    at Object.HeroService_Factory [as factory] (hero.service.ts:11:25)
    at core.mjs:3219:47
    at runInInjectorProfilerContext (core.mjs:866:9)
    at R3Injector.hydrate (core.mjs:3218:21)
    at R3Injector.get (core.mjs:3082:33)

Wir versuchen den HttpClient aus dem HttpClientModule in den Konstruktor von HeroService zu injizieren, aber es gibt keinen Provider für das HttpClientModule. Die Fehlermeldung gibt auch schon einen Hinweis darauf, wo das Problem zu fixen ist, nämlich in src/main.ts, im Aufruf von bootstrapApplication().

Weil die Applikations-Konfiguration aus src/main.ts ausgelagert ist, müssen wir stattdessen src/app/app.config.ts anpassen, und den Provider dort konfigurieren, genau wie bereits mit dem RouterModule geschehen.

export const appConfig: ApplicationConfig = {
    providers: [provideRouter(routes), provideHttpClient()],
};

Die Funktion provideHttpClient() muss aus @angular/common/http importiert werden.

Die Applikation funktioniert aber noch immer nicht. Die JavaScript-Konsole zeigt einen weiteren Fehler:

ERROR HttpErrorResponse {headers: _HttpHeaders, status: 200, statusText: 'OK', url: 'http://localhost:4200/api/heroes', ok: false, …}

Die Fehlermeldung ist nicht sehr hilfreich, aber das Problem ist, dass es keinen Provider für das HttpClientInMemoryWebApiModule gibt, das hier verwendet wird. Das Vorgehen weicht ein bisschen von dem für RouterModule und HttpClientModule ab, weil wir ein Argument an die Methode forRoot() des HttpClientInMemoryWebApiModules übergeben müssen. So geht es:

import { ApplicationConfig, importProvidersFrom } from '@angular/core';
import { provideRouter } from '@angular/router';

import { routes } from './app.routes';
import { provideHttpClient } from '@angular/common/http';
import { HttpClientInMemoryWebApiModule } from 'angular-in-memory-web-api';
import { InMemoryDataService } from './in-memory-data.service';

export const appConfig: ApplicationConfig = {
    providers: [
        provideRouter(routes),
        provideHttpClient(),
        importProvidersFrom([
            HttpClientInMemoryWebApiModule.forRoot(InMemoryDataService, {
                dataEncapsulation: false,
            }),
        ]),
    ],
};

Das ist dann auch schon die finale Version von src/app/app.config.ts.

Neue Helden erzeugen

Bei der Implementiert der Methode add() in src/app/heroes/heroes.component.ts, kommt es zu einem weiteren Fehler:

✘ [ERROR] TS2532: Object is possibly 'undefined'. [plugin angular-compiler]

Das beheben wir, in dem wir an heroes ein Fragezeichen anhängen, und es damit als optional kennzeichnen:

add(name: string): void {
        name = name.trim();
        if (!name) {
            return;
        }
        this.heroService.addHero({ name } as Hero).subscribe(hero => {
            this.heroes?.push(hero);
        });
    }

Suche implementieren

Wenn die Suche ins Dashboard integriert wird, gibt es wieder einen Fehler:

✘ [ERROR] NG8001: 'app-hero-search' is not a known element:

Die Lösung ist jetzt klar. Wir passen src/app/dashboard/dashboard.component.ts an.

@Component({
    selector: 'app-dashboard',
    standalone: true,
    imports: [NgFor, RouterModule, HeroSearchComponent],
    templateUrl: './dashboard.component.html',
    styleUrls: ['./dashboard.component.css'],
})

Beim Zufügen von routerLink zum Template von HeroSearchComponent bekommen wir diesen jetzt altbekannten Fehler:

✘ [ERROR] NG8002: Can't bind to 'routerLink' since it isn't a known property of 'a'. [plugin angular-compiler]

Dieser aber ist neu:

✘ [ERROR] NG8004: No pipe found with name 'async'. [plugin angular-compiler]

Beides muss in src/app/hero-search/hero-search.component.ts gefixt werden:

@Component({
    selector: 'app-hero-search',
    standalone: true,
    imports: [CommonModule, RouterModule, NgFor],
    templateUrl: './hero-search.component.html',
    styleUrl: './hero-search.component.css',
})

Wenn wir schon einmal dabei sind, importieren wir auch NgFor, um die hässliche Warnung, die wir bereits kennen, loszuwerden.

Die Applikation sollte jetzt funktionieren. Glückwunsch!

Leave a comment
Diese Website verwendet Cookies und ähnliche Technologien, um gewisse Funktionalität zu ermöglichen, die Benutzbarkeit zu erhöhen und Inhalt entsprechend ihren Interessen zu liefern. Über die technisch notwendigen Cookies hinaus können abhängig von ihrem Zweck Analyse- und Marketing-Cookies zum Einsatz kommen. Sie können ihre Zustimmung zu den vorher erwähnten Cookies erklären, indem sie auf "Zustimmen und weiter" klicken. Hier können sie Detaileinstellungen vornehmen oder ihre Zustimmung - auch teilweise - mit Wirkung für die Zukunft zurücknehmen. Für weitere Informationen lesen sie bitte unsere Datenschutzerklärung.