1. Angular Tutorial – Teil 1: Werkzeuge und erste Schritte
  2. Angular Tutorial – Teil 2: Eine erste Anwendung mit Komponenten, Datenbindung und HTTP-Zugriff
  3. Angular Tutorial – Teil 3: Wiederverwendbare Komponenten mit @Input und @Output

Angular Tutorial – Teil 3: Wiederverwendbare Komponenten mit @Input und @Output

  1. Angular Tutorial – Teil 1: Werkzeuge und erste Schritte
  2. Angular Tutorial – Teil 2: Eine erste Anwendung mit Komponenten, Datenbindung und HTTP-Zugriff
  3. Angular Tutorial – Teil 3: Wiederverwendbare Komponenten mit @Input und @Output

Sie wissen jetzt, wie Datenbindung unter Angular funktioniert. In diesem Abschnitt zeigen wir, wie Sie eigene Komponenten schreiben, die über Bindings mit der Außenwelt kommunizieren.

Dazu entwickeln wir eine Komponente, die Property- sowie Event-Bindings unterstützt. Außerdem werden wir ein Two-Way-Binding einführen.

Überblick

Dazu wird eine Komponente geschaffen, die so einfach wie möglich, aber so komplex wie nötig ist, um die vorhin diskutierten Konzepte zu zeigen. Es handelt sich dabei um eine Komponente, die Flüge in Form von Karten
präsentiert.

Die FlightCardComponent

Solche Karten sind derzeit sehr üblich, zumal sie ein flexibles (responsive) Design erlauben: Steht am Endgerät viel Platz zur Verfügung, kann eine Anwendung mehrere Karten nebeneinander anzeigen.
Steht wenig Platz zur Verfügung, zeigt die Anwendung die Karten untereinander an.

Vorbereitungen

Jede Karte kann ausgewählt werden. Wurde sie ausgewählt, erhält sie einen beigen Hintergrund, ansonsten einen weißen. Außerdem sollen alle ausgewählten Flüge im Warenkorb präsentiert werden. Dazu wird der Warenkorb auf ein Objekt abgeändert, das die IDs der Flüge auf einen boolean abbildet:

[...]
export class FlightSearchComponent implements OnInit {

    from = 'Hamburg';
    to = 'Graz';
    flights: Array<Flight> = [];
    selectedFlight: Flight | null = null;

    basket: { [key: number]: boolean } = {
        3: true,
        5: true
    };

    [...]

}

Im gezeigten Beispiel befinden sich von Anfang an die Flüge 3 und 5 im Warenkorb. Das soll das Ausprobieren unserer Anwendung ein wenig vereinfachen.

Der Datentyp von basket verdient unsere Aufmerksamkeit: { [key: number]: boolean } bedeutet, dass es sich hierbei um ein Objekt handelt, das Schlüssel vom Typ number auf Werte vom Typ boolean abbildet. Das Objekt wird also als Dictionary verwendet.

Genau genommen sind Objekte in JavaScript nichts anderes als Dictionaries. Normalerweise bilden sie die Namen von Eigenschaften auf deren Werte und die Namen von Methoden auf deren Implementierungen ab.

Eventuell fragen Sie sich, wie es möglich ist, dass die Schlüssel hier numbers sind, zumal in JavaScript die Schlüssel von Objekten ausschließlich Strings sind. Dazu müssen Sie sich vor Augen halten, dass wir es mit einem TypeScript-Compiler beim Kompilieren zu tun haben, und dieser erzwingt hier numbers. Zur Laufzeit haben wir es mit JavaScript zu tun. Dieses wandelt die numbers intern in Strings um.

Alternative Schreibweise

Falls Ihnen die hier verwendete Schreibweise zu unübersichtlich ist, können Sie auch in einem vorgelagerten Schritt einen Typ für das Dictionary definieren und dann basket damit typisieren:

type NumberBooleanDict = { [key: number]: boolean };

[...]

export class FlightSearchComponent implements OnInit {
    [...]
    basket: NumberBooleanDict = {
        3: true,
        5: true
    }
    [...]
}

Um festzustellen, ob sich ein Flug im Warenkorb befindet, muss die Anwendung also nur prüfen, ob der Basket an der Stelle der FlugId truthy ist:

const inBasket = this.basket[7]; // 7 ist eine FlugId.

Zur Visualisierung des Warenkorbs kommt aus Gründen der Vereinfachung abermals die JSON-Pipe zum Einsatz:

{{ basket | json }}

Das Ganze gestaltet sich dann wie folgt:

Ausgabe des Warenkorbs

Eine Komponente mit Property-Bindings Property-Binding

Die hier besprochene Karte, deren Implementierung im nächsten Abschnitt folgt, soll über Property-Bindings zwei Informationen vom Parent übergeben bekommen: den anzuzeigenden Flug und die Information, ob sie ausgewählt wurde. Für die erste Information weist die Komponente eine Eigenschaft flight und für zweite Information eine Eigenschaft selected auf:

<div *ngFor="let f of flights">
    <app-flight-card [flight]="f" [selected]="basket[f.id]">
    </app-flight-card>
</div>

Um alle gefundenen Flüge auszugeben, iteriert das betrachtete Beispiel über die Auflistung flights und gibt pro Eintrag eine Karte aus.

So können Sie sich das Einbinden einer Komponente wie den Aufruf einer Funktion vorstellen, die Parameter übergeben bekommt und ein Stück UI rendert. Eine andere Metapher für eine Komponente ist ein elektronisches Bauteil, z. B. ein Chip: Er ist über Eingänge mit der Außenwelt verdrahtet und bekommt auf diese Weise die nötigen Informationen.

Die Komponente flight-card nimmt Informationen über Eigenschaften entgegen.

Im hier betrachteten Fall nimmt der Eingang flight den jeweiligen Flug entgegen, und der Eingang selected bekommt den entsprechenden boolean aus dem Warenkorb.

Jetzt stellt sich natürlich die Frage, wie man mit Angular solche Eingänge darstellt. Der nächste Abschnitt geht darauf ein.

Implementierung der Komponente mit Property-Bindings

Unsere Komponente wird wieder mit der Angular CLI generiert:

ng g c flight-card

Alternativ dazu lässt sich, wie im ersten Teil dieses Tutorials gezeigt, das Visual-Studio-Plug-in Angular Schematics dafür nutzen. Es richtet für diese Aufgabe im Kontextmenü der einzelnen Ordner einen Befehl Angular: Generate a component ein.

Die Implementierung unserer flight-card besteht zunächst mal aus einer Klasse mit einem Component-Dekorator:

// 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() flight: Flight | null = null;
    @Input() selected = false;

    select() {
        this.selected = true;
    }

    deselect() {
        this.selected = false;
    }

}

Der Dekorator erhält einen Selektor sowie einen Verweis auf ein Template. Den von der CLI generierten Konstruktor sowie die Implementierung von OnInit OnInit haben wir entfernt, da sie hier nicht benötigt werden.

Bis hierhin bietet diese Implementierung nichts Neues. Neu ist allerdings der Input-Dekorator @Input. Er dekoriert sämtliche Eigenschaften, die die Komponente von ihrem Parent entgegennimmt.

Außerdem weist sie zwei Methoden auf, die ihr Template aufruft: select wählt die Karte aus, und deselect hebt diese Auswahl wieder auf.

Das Template dieser Komponente prüft zunächst, ob die Karte selektiert wurde. Ist dem so, erhält die Karte per ngClass eine entsprechende Formatierung:

<!-- src/app/flight-card/flight-card.component.html -->

<div class="card" [ngClass]="{ 'active-card' : selected }">

    <div class="card-header">
        <h2 class="title">{{flight?.from}} - {{flight?.to}}</h2>
    </div>

    <div class="card-body">
        <p>Flight-No.: #{{flight?.id}}</p>
        <p>Date: {{flight?.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>

Das Template gibt danach ein paar Daten des aktuellen Flugs aus. Bitte beachten Sie die Nutzung des Safe-Navigation-Operators (Fragezeichen): Statt flight.id kommt hier zum Beispiel flight?.id zum Einsatz. Das ist notwendig, weil die Eigenschaft flight initial null ist und null.id im Strict Mode nicht erlaubt ist. Stattdessen veranlasst der Safe-Navigation-Operator Angular, die Navigation abzubrechen und null zurückzuliefern.

Das Styling für die Klasse active-card kann wieder lokal in die Datei flight-card.component.scss oder global in die Datei styles.scss eingetragen werden:

.active-card {
    background-color: rgb(204, 197, 185);
}

Diese Farbe wurde so gewählt, dass sie zum verwendeten Theming passt. Die anderen hier verwendeten Klassen werden von der eingebundenen Styling-Bibliothek Bootstrap definiert.

Komponente registrieren und aufrufen

Auch diese Komponente muss bei einem Angular-Modul registriert werden. In unserem Fall handelt es sich um das AppModule. Der Aufruf von ng generate component oder die Nutzung des Plug-ins Angular Schematics in Visual Studio Code sollte diese Aufgabe automatisieren. Zur Sicherheit empfiehlt es sich jedoch, diesen Umstand zu prüfen:

// src/app/app.module.ts

[...]
import { FlightCardComponent } from './flight-card/flight-card.component';

@NgModule({
    imports: [
        [...]
    ],
    declarations: [
        [...]
        FlightCardComponent
    ],
    providers: [],
    bootstrap: [
        AppComponent
    ]
})
export class AppModule { }

Danach erhält das gesamte Modul Zugriff auf die Komponente und lässt sich zur Präsentation gefundener Flüge in der FlightSearchComponent verwenden:

<div *ngFor="let f of flights">
    <app-flight-card [flight]="f" [selected]="basket[f.id]">
    </app-flight-card>
</div>

Wie besprochen, erhält diese Komponente den aktuellen Flug und den Boolean aus dem Warenkorb. Die Anwendung sollte nun die gefundenen Flüge als Karten präsentieren.

Die Karten lassen sich auch über die präsentierten Schaltflächen aus- und abwählen. Ein kleines Problem fällt dabei allerdings auf: Angular aktualisiert die Eigenschaft basket und somit den präsentierten Warenkorb am Ende der Seite nicht. Hierzu müsste die FlightCardComponent ihren Parent, der den Warenkorb verwaltet, mit einem Ereignis benachrichtigen. Wie das geht, erläutert der nächste Abschnitt.

Bonus: Bootstrap Grid Layout

Falls Sie dieses Beispiel nachstellen, fällt Ihnen gegebenenfalls auf, dass die einzelnen Karten sehr viel Platz benötigen:

Um mehrere Karten nebeneinander zu präsentieren, kann man zum Spaltenlayout von Bootstrap Bootstrap greifen. Es ist für responsive Designs gedacht – also für Designs, die sich an unterschiedliche Auflösungen anpassen. Dazu unterteilt es eine Seite in zwölf gedachte Spalten, und die Anwendung weist jedem Element eine bestimmte Anzahl an Spalten zu. Dabei kann es zwischen sehr kleinen (extra small, xs), kleinen (small, sm), mittleren (medium, md), großen (large, lg) und sehr großen (extra large, xl) Bildschirmen unterscheiden. Beispiele für diese Größeneinheiten sind Handys (xs), Tablets (sm und md) sowie Laptops und Desktopgeräte (lg und xl). Hierbei handelt es sich jedoch nur um Näherungen, denn schlussendlich kommt es auf die zur Verfügung stehende Auflösung an.

Beispielsweise könnte man nun angeben, dass eine Karte bei sehr kleinen Geräten (xs) alle zwölf Spalten erhält, bei kleinen (sm) sechs, bei mittleren (md) sowie bei großen (lg) vier und bei sehr großen (lg und xl) drei der insgesamt zwölf Spalten. Somit werden je nach Auflösung eine bis vier Karten nebeneinander präsentiert. Hierzu sieht Bootstrap die nachfolgend verwendeten Klassen vor:

<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 [flight]="f" [selected]="basket[f.id]">
        </app-flight-card>
    </div>
</div>

Jede dieser Klassen, die mit dem Präfix col- eingeleitet werden, gibt für eine Auflösung die gewünschte Spaltenanzahl an. Beispielsweise bedeutet col-md-4, dass eine Karte bei einem mittleren Gerät vier der zwölf Spalten erhält.

Außerdem sind die einzelnen Spalten in einen Container, z. B. ein div, mit der Klasse row zu platzieren. Sie kümmert sich darum, dass bei Bedarf eine neue Zeile mit Flugkarten begonnen wird.

Das Ergebnis dieses Vorgehens sieht bei einem Bildschirm mit der Auflösung lg wie folgt aus:

Komponenten mit Event-Bindings Event-Binding

Dieser Abschnitt erweitert die hier gezeigte FlightCardComponent um ein Ereignis selectedChange. Dieses Ereignis soll den Parent informieren, wenn die Karte aus- bzw. abgewählt wird:

<div *ngFor="let f of flights">
    <app-flight-card [flight]="f"
        [selected]="basket[f.id]"
        (selectedChange)="basket[f.id] = $event">
    </app-flight-card>
</div>

Das Event selectedChange werden wir gleich einführen. Warten Sie bis dahin bitte mit dem hier gezeigten Aufruf, um Kompilierungsfehler zu vermeiden.

Man könnte sich diese eine Komponente als Funktion vorstellen, die einen Callback selectedChange übergeben bekommt. Immer wenn sie aus- bzw. abgewählt wird, ruft sie diesen Callback auf.

Die Metapher mit dem Chip passt hier noch besser: Ein Chip hat Ein- und Ausgänge, über die er mit seiner Umgebung verdrahtet wird. Die Ausgänge entsprechen den Events. Im hier betrachteten Fall fließt der Wert selected über einen Ausgang zurück in den Warenkorb.

Komponente mit Eingängen (Properties) und einem Ausgang
(Event)

Implementierung der Komponente mit Event-Binding

Für das Event erhält die FlightCardComponent eine Eigenschaft
selectedChange, die Sie mit Output dekorieren müssen:

// 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() flight: 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 Typ des Output @Output ist per definitionem ein EventEmitter EventEmitter. Da es mehrere Typen mit diesem allgemeinen Namen gibt, sollten Sie sich vergewissern, dass Sie den Typ EventEmitter aus @angular/core importieren. Gerade beim Einsatz von Auto-Imports schlagen Entwicklungsumgebungen wie Visual Studio Code häufig den falschen Paketnamen vor.

Damit der EventEmitter den neuen Wert von selected veröffentlichen kann, wird er mit Boolean typisiert.

Komponente aufrufen

Nach dieser Erweiterung können Sie mit dem Aufruf der FlightCardComponent einen Event-Handler für selectedChange festlegen:

<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
            [flight]="f"
            [selected]="basket[f.id]"
            (selectedChange)="basket[f.id] = $event">
        </app-flight-card>

    </div>
</div>

Die von Angular eingerichtete Variable $event beinhaltet den an emit übergebenen Wert, also true oder false. Die Anwendung sollte nun beim Aus- und Abwählen einer Karte den Warenkorb aktualisieren:

Der Warenkorb wird nun aktualisiert.

Komponenten mit Two-Way-Bindings Two-Way-Binding

Unsere Input/Output-Kombination für selected erfüllt sämtliche Konventionen für die verkürzte Banana-in-a-Box-Schreibweise ("Two-Way-Binding"): Das Event setzt sich aus dem Namen der Property sowie aus dem Suffix Change zusammen und veröffentlicht den geänderten Wert via $event. Insofern spricht hier nichts gegen den Einsatz dieser komfortablen Grammatik:

<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
            [flight]="f"
            [(selected)]="basket[f.id]">
        </app-flight-card>

    </div>
</div>

Hier zeigt sich auch der Nachteil dieser Abkürzung: Sie schreibt nach jeder Änderung den neuen Wert direkt in die Ausgangsvariable zurück. Wollte man hingegen zur Aktualisierung eine Methode anstoßen, müsste man das stattdessen explizit mit einem Event-Binding erledigen.

Don't Miss Anything!


Subscribe to our newsletter to get all the information about Angular.


* By subscribing to our newsletter, you agree with our privacy policy.

Unsere Angular-Schulungen

No post was found with your current grid settings. You should verify if you have posts inside the current selected post type(s) and if the meta key filter is not too much restrictive.
  1. Angular Tutorial – Teil 1: Werkzeuge und erste Schritte
  2. Angular Tutorial – Teil 2: Eine erste Anwendung mit Komponenten, Datenbindung und HTTP-Zugriff
  3. Angular Tutorial – Teil 3: Wiederverwendbare Komponenten mit @Input und @Output

Current Blog Articles

  1. Angular Tutorial – Teil 1: Werkzeuge und erste Schritte
  2. Angular Tutorial – Teil 2: Eine erste Anwendung mit Komponenten, Datenbindung und HTTP-Zugriff
  3. Angular Tutorial – Teil 3: Wiederverwendbare Komponenten mit @Input und @Output

Only One Step Away!

Send us your inquery today - we help you with pleasure!

Jetzt anfragen!