In diesem Abschnitt möchten wir die Anwendung mithilfe der Standardfunktionen von Angular optimieren. Dabei verschieben wir wiederverwendbare UI-Bausteine in eine Sub-Komponente, während wir wiederverwendbare Geschäftslogik in einem Service bündeln.
Wie zuvor findest du den vollständigen Code wieder auf GitHub.
Sub-Komponenten mit Event- und Property-Bindings
Jede Angular-Komponente hat die Möglichkeit, innerhalb ihres Templates auf weitere Komponenten zu verweisen. Für den Datenaustausch nutzt man dabei die bereits im vorherigen Teil behandelten Property- und Event-Bindings. Zur Veranschaulichung dient eine Sub-Komponente, die Flüge als Karten darstellt:

Card layouts like these are a common pattern today, partly because they enable a responsive design: when the device offers ample screen space, an application can position several cards side by side. If space is limited, the application stacks the cards vertically.
Getting ready
Each card is clickable and selectable. Upon selection, its background changes to beige; otherwise, it stays white. In addition, every chosen flight should be listed in the shopping cart. To achieve this, we refactor the cart into an object that maps the flight IDs to a boolean value:
[...]
export class FlightSearchComponent implements OnInit {
from = 'Hamburg';
to = 'Graz';
flights: Array<Flight> = [];
selectedFlight: Flight | null = null;
basket: { [key: number]: boolean } = {
3: true,
5: true
};
[...]
}
In the example shown, flights 3 and 5 are already placed in the basket from the start. This is intended to make trying out our application a bit easier.
The data type of basket deserves our attention: { [key: number]: boolean } indicates that this is an object mapping keys of type number to values of type boolean. Thus, the object is used as a dictionary.
Become an Angular Pro with our trainings
From the Angular Practice Training for Beginners & Self-Taught Developers via Advanced Workshops for Experienced Developers up to Deep Dives for Advanced Users. In our Angular Trainings participants learn everything a developer needs for successful Angular projects.
If you find the notation used here too confusing, you can also define a type for the dictionary in a preliminary step and then type basket accordingly:
type NumberBooleanDict = { [key: number]: boolean };
[...]
export class FlightSearchComponent implements OnInit {
[...]
basket: NumberBooleanDict = {
3: true,
5: true
}
[...]
}
Um zu ermitteln, ob ein Flug bereits im Warenkorb liegt, genügt es also, dass die Anwendung kontrolliert, ob der Basket an der entsprechenden FlugId-Position einen truthy Wert besitzt:
const inBasket = this.basket[7]; // 7 ist eine FlugId.
Für die Darstellung des Warenkorbs wird wiederum die JSON-Pipe verwendet, um die Komplexität gering zu halten:
{{ basket | json }}
Das sieht dann so aus, wie im folgenden Beispiel dargestellt:

Eine Komponente mit Property-Bindings Property-Binding
Die Karte, deren Code im nächsten Abschnitt vorgestellt wird, erhält über Property-Bindings zwei Dateneingaben vom übergeordneten Element: das anzuzeigende Flugobjekt und den Auswahlstatus. Dafür definiert die Komponente die Property item für den Flug und die Property selected für die Auswahl.
<div *ngFor="let f of flights">
<app-flight-card [item]="f" [selected]="basket[f.id]">
</app-flight-card>
</div>
Um sämtliche ermittelten Flüge darzustellen, durchläuft das vorgestellte Beispiel die Liste flights und rendert für jeden Datensatz eine Karte.
Stellen Sie sich das Einbinden einer Komponente wie einen Funktionsaufruf vor, dem Parameter übergeben werden und der eine UI-Sektion erzeugt. Alternativ lässt sich eine Komponente mit einem elektronischen Modul wie einem Chip vergleichen: Über seine Pins ist er mit der Umgebung verbunden und erhält dadurch alle relevanten Daten:

Der Eingang item erhält in diesem Szenario den betreffenden Flug, während selected den zugehörigen boolean-Wert aus dem Warenkorb übernimmt.
Property-Bindings in der Komponente umsetzen
Zur Generierung der Komponente kommt erneut die Angular CLI zum Einsatz:
ng g c flight-card
Eine weitere Möglichkeit ist die Verwendung des Visual-Studio-Plug-ins Angular Schematics, wie im vorherigen Teil dieser Serie beschrieben. Dieses fügt im Kontextmenü der jeweiligen Verzeichnisse einen Eintrag Angular: Generate a component hinzu.
Unsere flight-card wird zu Beginn als Klasse mit einem Component-Dekorator implementiert:
// src/app/flight-card/flight-card.component.ts
import { Component, Input } from '@angular/core';
import { Flight } from '../flight';
@Component({
selector: 'app-flight-card',
templateUrl: './flight-card.component.html',
styleUrls: ['./flight-card.component.scss']
})
export class FlightCardComponent {
@Input() item: Flight | null = null;
@Input() selected = false;
select() {
this.selected = true;
}
deselect() {
this.selected = false;
}
}
Der Dekorator definiert einen Selektor und verweist auf ein Template. Die CLI hat standardmäßig einen Konstruktor und eine OnInit-Implementierung generiert, die wir hier jedoch gelöscht haben, weil sie in diesem Fall überflüssig sind.
Diese Version der Komponente unterscheidet sich bislang nicht von früheren Beispielen. Der Input-Dekorator stellt jedoch eine Neuerung dar, da er alle Eigenschaften markiert, die von der übergeordneten Komponente übergeben werden.
Zusätzlich besitzt die Komponente zwei Methoden, die im Template aufgerufen werden: select markiert die Karte als ausgewählt, während deselect diese Markierung entfernt.
Das zugehörige Template prüft zuerst, ob eine Karte ausgewählt ist. Wenn das zutrifft, wird die Karte mithilfe von ngClass mit einer passenden Gestaltung versehen:
<!-- src/app/flight-card/flight-card.component.html -->
<div class="card" [ngClass]="{ 'active-card' : selected }">
<div class="card-header">
<h2 class="title">{{item?.from}} - {{item?.to}}</h2>
</div>
<div class="card-body">
<p>Flight-No.: #{{item?.id}}</p>
<p>Date: {{item?.date | date:'dd.MM.yyyy HH:mm'}}</p>
<p>
<button class="btn btn-default"
*ngIf="!selected"
(click)="select()">Select</button>
<button class="btn btn-default"
*ngIf="selected"
(click)="deselect()">Remove</button>
</p>
</div>
</div>
Nach der Verarbeitung zeigt das Template einige Daten des momentanen Flugs an. Wichtig ist der Einsatz des Safe-Navigation-Operators (Fragezeichen): Anstelle von item.id wird beispielsweise item?.id verwendet. Dies ist erforderlich, da die Eigenschaft item anfänglich den Wert null besitzt und null.id im Strict Mode nicht zulässig ist. Der Safe-Navigation-Operator sorgt daher dafür, dass Angular die Navigation stoppt und null zurückgibt.
Das Aussehen für die CSS-Klasse active-card lässt sich entweder in der lokalen Datei flight-card.component.scss oder der globalen Datei styles.scss definieren:
.active-card {
background-color: rgb(204, 197, 185);
}
Die Auswahl dieser Farbe erfolgte passend zum aktiven Theming. Die übrigen verwendeten Klassen stammen aus der eingebundenen Styling-Bibliothek Bootstrap.
Komponente registrieren und aufrufen
Diese Komponente muss ebenso in einem Angular-Modul registriert sein. In unserem Fall ist dies das AppModule. Für gewöhnlich übernimmt die CLI diese Aufgabe automatisch. Es ist dennoch ratsam, dies sicherheitshalber zu überprüfen:
// src/app/app.module.ts
[...]
import { FlightCardComponent } from './flight-card/flight-card.component';
@NgModule({
imports: [
[...]
],
declarations: [
[...]
FlightCardComponent
],
providers: [],
bootstrap: [
AppComponent
]
})
export class AppModule { }
Von diesem Punkt an steht die Komponente dem gesamten Modul zur Verfügung und kann innerhalb der FlightSearchComponent genutzt werden, um die ermittelten Flüge darzustellen:
<div *ngFor="let f of flights">
<app-flight-card [item]="f" [selected]="basket[f.id]">
</app-flight-card>
</div>
Die Komponente erhält, wie bereits erläutert, den aktuellen Flug sowie den Boolean, der den Warenkorb-Status widerspiegelt. Die Anwendung präsentiert die gefundenen Flüge nun in Kartenform.
Über die angezeigten Buttons lassen sich diese Karten aus- und wieder abwählen. Dabei tritt allerdings ein Problem auf: Angular erneuert die Eigenschaft basket und damit die Darstellung des Warenkorbs am Seitenende nicht. Damit die FlightCardComponent ihren übergeordneten Warenkorb-Verwalter über Ereignisse informieren kann, ist eine entsprechende Benachrichtigung erforderlich. Wie diese Umsetzung erfolgt, wird an späterer Stelle in diesem Kapitel gezeigt.
Zusätzlich: Bootstrap Grid Layout für ein adaptives Design
Wenn Sie das Beispiel eigenständig nachbauen, wird Ihnen möglicherweise auffallen, dass die einzelnen Karten überaus viel Fläche einnehmen:

Wenn man mehrere Karten nebeneinander anzeigen möchte, bietet sich das Spaltensystem von Bootstrap an. Es ist für responsive Layouts konzipiert – also für Layouts, die sich verschiedenen Bildschirmgrößen anpassen. Dabei wird die Seite in zwölf imaginäre Spalten unterteilt, und jede Komponente bekommt eine festgelegte Anzahl dieser Spalten zugewiesen. Unterschieden wird zwischen sehr kleinen (extra small, xs), kleinen (small, sm), mittleren (medium, md), großen (large, lg) und sehr großen (extra large, xl) Bildschirmen. Typische Beispiele für diese Größenklassen sind Smartphones (xs), Tablets (sm und md) sowie Laptops oder Desktops (lg und xl). Es handelt sich hierbei nur um ungefähre Einteilungen, da letztlich die tatsächlich verfügbare Auflösung entscheidend ist.
So könnte man etwa festlegen, dass eine Karte auf sehr kleinen Displays (xs) die vollen zwölf Spalten einnimmt, auf kleinen (sm) sechs, auf mittleren (md) und großen (lg) jeweils vier und auf sehr großen (xl) nur drei der zwölf Spalten. Auf diese Weise erscheinen je nach Bildschirmgröße ein bis vier Karten in einer Reihe. Hierfür stellt Bootstrap die im Folgenden genutzten CSS-Klassen zur Verfügung:
<div class="row">
<div *ngFor="let f of flights"
class="col-xs-12 col-sm-6 col-md-4 col-lg-4 col-xl-3">
<app-flight-card [item]="f" [selected]="basket[f.id]">
</app-flight-card>
</div>
</div>
Jede der mit dem Präfix col- versehenen Klassen legt für eine bestimmte Bildschirmgröße fest, wie viele Spalten das jeweilige Element belegt. So zeigt etwa col-md-4 an, dass eine Karte auf einem Gerät mittlerer Größe vier der insgesamt zwölf Spalten einnimmt.
Zudem müssen die einzelnen Spalten innerhalb eines Containers liegen, etwa eines div-Elements, dem die Klasse row zugewiesen wurde. Diese sorgt dafür, dass bei Bedarf automatisch eine frische Zeile für weitere Flugkarten beginnt.
Auf einem Display mit der Breitenklasse lg ergibt sich damit das folgende Erscheinungsbild:

Komponenten mit Event-Bindings (Event-Binding)
In diesem Teil wird die FlightCardComponent aus dem vorherigen Beispiel um ein selectedChange-Event ergänzt. Dadurch erhält die übergeordnete Komponente eine Benachrichtigung, sobald die Karte markiert oder die Markierung aufgehoben wird.
<div *ngFor="let f of flights">
<app-flight-card [item]="f"
[selected]="basket[f.id]"
(selectedChange)="basket[f.id] = $event">
</app-flight-card>
</div>
Das Event selectedChange wird weiter unten eingeführt. Bitte setzen Sie den hier gezeigten Aufruf erst dann ein, wenn dieser definiert ist, um Kompilierungsfehler zu vermeiden.
Stellen Sie sich diese Komponente wie eine Funktion vor, der ein Callback selectedChange übergeben wird. Bei jedem Aus- oder Abwählen aktiviert sie diesen Callback.
Die Chip-Metapher trifft hier eher zu: Ein Chip besitzt Ein- und Ausgänge, über die er mit der Umgebung verbunden wird. Die Ausgänge repräsentieren die Events. In diesem Fall fließt der Wert selected über einen Ausgang zurück in den Warenkorb:

Implementierung der Komponente mit Event-Binding
Die FlightCardComponent benötigt für das Event die Eigenschaft selectedChange, welche durch die Dekoration mit Output versehen wird:
// src/app/flight-card/flight-card.component.ts
import { Component, Input, Output, EventEmitter } from '@angular/core';
import { Flight } from '../flight';
@Component({
selector: 'app-flight-card',
templateUrl: './flight-card.component.html',
styleUrls: ['./flight-card.component.scss']
})
export class FlightCardComponent {
@Input() item: Flight | null = null;
@Input() selected = false;
@Output() selectedChange = new EventEmitter<boolean>();
select() {
this.selected = true;
this.selectedChange.emit(true);
}
deselect() {
this.selected = false;
this.selectedChange.emit(false);
}
}
Der Output besitzt definitionsgemäß den Typ EventEmitter. Weil mehrere Typen diesen allgemeinen Namen tragen, ist es wichtig, den Typ EventEmitter aus @angular/core zu importieren. Vor allem bei Auto-Imports neigen Entwicklungswerkzeuge wie Visual Studio Code dazu, fälschlicherweise den falschen Paketnamen vorzuschlagen.
Für die Veröffentlichung des neuen Werts von selected durch den EventEmitter erfolgt eine Typisierung mit Boolean.
Aufruf der Komponente
Nach dieser Erweiterung lässt sich beim Aufruf der FlightCardComponent ein Event-Handler für selectedChange definieren:
<div class="row">
<div
*ngFor="let f of flights"
class="col-xs-12 col-sm-6 col-md-4 col-lg-4 col-xl-3">
<app-flight-card
[item]="f"
[selected]="basket[f.id]"
(selectedChange)="basket[f.id] = $event">
</app-flight-card>
</div>
</div>
Die von Angular bereitgestellte Variable $event enthält den Wert, der an emit weitergegeben wurde – in diesem Fall entweder true oder false. Jetzt sollte die Applikation den Warenkorb immer dann neu berechnen, wenn eine Karte markiert oder ihre Markierung entfernt wird:

Komponenten mit Two-Way-Bindings
Hier gibt es eine erfreuliche Nachricht: Unsere Input/Output-Kombination mit selected und selectedChange folgt genau den erforderlichen Regeln für die abgekürzte Banana-in-a-Box-Syntax. Das Event muss lediglich denselben Namen wie die Property tragen, ergänzt um das Suffix Change, und den aktualisierten Wert über $event übermitteln. Damit steht einem Einsatz dieser bequemen Schreibweise grundsätzlich nichts entgegen:
<div class="row">
<div
*ngFor="let f of flights"
class="col-xs-12 col-sm-6 col-md-4 col-lg-4 col-xl-3">
<app-flight-card
[item]="f"
[(selected)]="basket[f.id]">
</app-flight-card>
</div>
</div>
Die Syntax für Two-Way-Bindings stellt im Grunde nur eine Abkürzung dar, die Angular dann zur Verfügung stellt, wenn bestimmte Konventionen erfüllt sind.
Wiederverwendbare Logik in Services auslagern
Bisher haben wir sämtliche Programmlogik direkt in Komponenten implementiert. Wenn jedoch dieselben Abläufe in mehreren Komponenten gebraucht werden, empfiehlt es sich, diese an einer zentralen Stelle zu bündeln. Zu diesem Zweck bietet Angular das Konzept der Services an, bei denen es sich meist um wiederverwendbare Klassen handelt.
Im Folgenden erfahren Sie, wie Sie selbst Services definieren und mithilfe von Dependency Injection einsetzen.
Ein erster Service
Derzeit übernimmt unsere FlightSearchComponent selbst den Abruf der Flüge über HTTP. Allerdings ist es wahrscheinlich, dass andere Komponenten in Zukunft ebenfalls auf dieselben Serverzugriffe angewiesen sind. Daher ist es üblich, derartige Aufgaben in dedizierte Services zu verlagern.
Genau das ist die erste Aufgabe dieses Abschnitts. Ähnlich wie Komponenten lassen sich auch Services mithilfe der Angular CLI erzeugen. Führen Sie dazu den nachfolgenden Befehl im Projektverzeichnis aus:
ng generate service flight
Es gibt auch eine verkürzte Schreibweise für den Befehl zum Erzeugen eines Service:
ng g s flight
Zusätzlich haben Sie die Möglichkeit, Services über das Kontextmenü eines Ordners in Visual Studio Code zu generieren, wenn das Plug-in Angular Schematics eingerichtet wurde:

Mit diesem Befehl werden zwei Dateien durch die CLI erzeugt:

Das durch die Angular CLI erzeugte File flight.service.ts stellt das fundamentale Skelett für unseren neu definierten FlightService bereit.
// src/app/flight.service.ts
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class FlightService {
constructor() { }
}
Die Einrichtung von Services wird auch als Provider bzw. Serviceprovider bezeichnet. Wenn der Service über die Eigenschaften von Injectable konfiguriert wird, spricht man von Tree-Shakable Providern. Diese Bezeichnung entstand, weil solche Provider optimal mit Tree-Shaking, einer Optimierungsmethode, zusammenarbeiten. Bei dieser Methode entfernt der Kompiler alle unnötigen Framework-Teile, was zu kleineren Bundles führt. Die Angular CLI übernimmt diese Aufgabe automatisch, wenn die Bundles mittels ng build erstellt werden.
Im vorliegenden Beispiel handelt es sich um eine Klasse, die lediglich mit einem Injectable-Dekorator versehen ist. Angular erkennt durch diesen Dekorator, dass die Klasse als Service dienen soll.
Die providedIn-Eigenschaft definiert den Scope des Services. Mit anderen Worten: providedIn zeigt auf, in welchem Bereich der Anwendung der Service verfügbar ist. Üblicherweise begegnen Ihnen die folgenden zwei Varianten:
- root (String): Der String root
rootbedeutet, dass derFlightServicein der gesamten Anwendung zugänglich ist. Dies wird auch alsRoot-Scopebezeichnet. Diese Option werden Sie meistens verwenden. - Verweis auf ein lazy Angular-Modul: Eine Anwendung lässt sich so konfigurieren, dass sie ein Angular-Modul erst dann in den Browser lädt, wenn es benötigt wird — das nennt man lazy loading. Wenn
providedInauf ein solches Modul verweist, wird der Service zusammen mit dem Modul nachgeladen und ist nur innerhalb dieses Moduls einsetzbar.
Es ist übrigens sinnlos,
providedInauf ein Modul zu setzen, das nicht per Lazy Loading bezogen wird. Da solche Module bereits zu Beginn verfügbar sind, teilen sie sich ohnehin den Root-Scope. Dies würde also lediglich denselben Effekt wieprovidedIn: rooterzeugen.
Jetzt lassen wir den FlightService mit einer Methode find zur Flugsuche ausstatten:
// src/app/flight.service.ts
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { Flight } from './flight';
@Injectable({
providedIn: 'root'
})
export class FlightService {
constructor(private http: HttpClient) { }
find(from: string, to: string): Observable<Flight[]> {
const url = 'http://demo.ANGULARarchitects.io/api/flight';
const headers = new HttpHeaders()
.set('Accept', 'application/json');
const params = new HttpParams()
.set('from', from)
.set('to', to);
return this.http.get<Flight[]>(url, {headers, params});
}
}
This new approach is essentially built the same way as the search method that we originally defined inside the FlightSearchComponent. Take note of the following details:
- An
HttpClientcan be injected into theFlightService. This shows that services are also able to pull in other services using dependency injection. - The
findmethod returns the output ofthis.http.getas anObservable<Flight>. Consequently, whoever callsfindhas to invokesubscribeon the returned observable to actually get the flights that were fetched.
Consuming the service
We can now make use of our FlightService inside the FlightSearchComponent:
// src/app/flight-search/flight-search.component.ts
import { Component, OnInit } from '@angular/core';
import { Flight } from '../flight';
import { FlightService } from '../flight.service';
@Component({
selector: 'app-flight-search',
templateUrl: './flight-search.component.html',
styleUrls: ['./flight-search.component.scss']
})
export class FlightSearchComponent implements OnInit {
from = 'Hamburg';
to = 'Graz';
flights: Array<Flight> = [];
selectedFlight: Flight | null = null;
basket: { [key: number]: boolean } = {
3: true,
5: true
};
constructor(private flightService: FlightService) {
}
ngOnInit(): void {
}
search(): void {
this.flightService.find(this.from, this.to).subscribe({
next: (flights) => {
this.flights = flights;
},
error: (err) => {
console.debug('Error', err);
}
});
}
select(f: Flight): void {
this.selectedFlight = f;
}
}
Die überarbeitete FlightSearchComponent kann den FlightService nun über ihren Konstruktor injizieren. Die search-Methode greift für die Flugsuche auf diesen FlightService zurück.
Der frühere HttpClient ist hinfällig geworden. Seine Nutzung wurde daher vollständig aus der FlightSearchComponent gestrichen. Ebenso entfällt die zuvor demonstrierte createDemoFlight-Funktion, die zur Anschauung einen neuen Flug erzeugte.
Glückwunsch! Ihr erster Service mit wiederverwendbarer Logik ist geschrieben und in einer Komponente zum Einsatz gekommen.
Rückblick und nächste Schritte
Innerhalb von Angular stehen diverse Bausteine bereit, um wiederverwendbare App-Teile zu gestalten: (Sub-)Komponenten enthalten wiederverwendbare UI-Ausschnitte und tauschen sich über Property- und Event-Bindings mit den übergeordneten Komponenten aus. Zu diesem Zweck markiert man sie mit den Dekoratoren @Input beziehungsweise @Output. Services dagegen bündeln wiederverwendbare Geschäftslogik und sind in weitere Services sowie Komponenten injizierbar.
Im folgenden Beitrag dieses Tutorials widmen wir uns einem weiteren Angular-Merkmal: dem Router. Mit ihm lassen sich unterschiedliche Seiten simulieren und Navigationshierarchien aufbauen.
