Angular: Input & Output

18.02.2021 Tom Trapp
Mobile angular typescript handson

Das sehr beliebte, Client-seitige JavaScript Framework Angular ist Ihnen noch kein Begriff? Sehen Sie sich unser Angular b-nova To Do List Tutorial an.

In diesem Blogpost wollen wir unsere To Do List etwas erweitern, wir erstellen eine zweite Komponente und wollen diese mit Daten "befüttern" und auf Events reagieren.

b-nova To Do List Item

Los gehts, wie Sie bereits aus dem Tutorial kennen erstellen wir mit ng eine neue Komponente, diese soll ein einzelnes To Do Item repräsentieren.

ng g c components/to-do-item

Glücklicherweise referenziert Angular über ihr CLI Tool bereits unsere neue Komponente im app.modules.ts File, daher können wir diese direkt einbinden. Dies machen wir, wie auch bei anderen Komponente, über den Selektor, unser ToDo-Item soll direkt in der ngFor Schleife der To Dos Komponente inkludiert werden.

<!-- to-dos.component.html -->
...
<div class="todo {{ toDo.completed ? 'done' : '' }}" *ngFor="let toDo of toDos; let i = index">
    <app-to-do-item></app-to-do-item>
...

Nun sollte unsere Komponente wie folgt aussehen, der Standardtext "to-to-item works!" sollte öfters ausgegeben werden.

Selbstverständlich ist dies aber nur die halbe Magie, nun wollen wir das HTML des To Do Items auch effektiv in der To Do Item Component haben.

Einfach verschieben? Probieren Sie es aus!

Ups, nun ist unsere To Do Liste kaputt und wir haben ein Haufen von Fehlern in der Konsole. Aber wie können wir die Daten einer Komponente zu einer anderen senden?

Input Bindings

Auch hier bietet Angular eine einfache aber mächtige Lösung, sogenannte Input Bindings. Diese erlauben uns, Daten in einer Komponente entgegenzunehmen und zu verwenden.

Schauen wir uns das HTML eines To Do Items nochmal genauer an, wir brauchen zwei Input Variablen, einmal das toDo selbst und einmal die Variable i welche für den Index steht.

// to-do-item.component.ts

...
export class ToDoItemComponent implements OnInit {

    @Input() toDo:ToDo = new ToDo;
    @Input() i:number = 0;
...

Nun müssen wir nur noch die Daten entsprechend an die Komponente geben, über ein sogenanntes Binding.

<!-- to-dos.component.html -->

<app-to-do-item [toDo]="toDo" [i]="i"></app-to-do-item>

Hier setzen wir nun die Variable toDo aus der ToDosComponent auf den Input Parameter toDo der ToDoItemComponent.

Unsere Application sieht nun fast wieder aus wie vorher, leider gibt es weiterhin Fehler, dass die Methoden zum Abhaken und Löschen eines Items nicht gefunden werden. Diese Funktionalität möchten wir gerne in der ToDosComponent behalten, somit muss die ToDoItemComponent irgendwie über eine solche Aktion des Nutzer informieren können.

Ein weiterer grosser Vorteil dieser Architektur ist, dass Daten nur einmalig geladen werden müssen.

Zusammenfassend gesagt senden wir hier Daten von der Parent zur Child Komponente.

Output Bindings

Das sogenannte Output Binding erlaubt es uns, eigene Event mit Daten einer Komponente nach Aussen für anderen Komponenten zu versenden.

Nach einem kurzen Blick auf unsere Item Komponente fällt uns auf, dass wir zwei Event haben, einmal wurde ein Item als Erledigt markiert und einmal wurde ein Item gelöscht. Selbstverständlich könnten wir dies in zwei separaten Event abhandeln, wir möchten dies aber 'lean' halten und nur ein Event mit einem Parameter nutzen.

Hierfür benötigen wir zwei neues Dateien, ein neues Model und ein neues Enum mit zwei vordefinierten Werten:

// src/app/models/ToDoEvent.ts
import { ToDoEventType } from "../components/enums/ToDoEventType";

export class ToDoEvent {
    type:ToDoEventType = ToDoEventType.COMPLETE;
    index:number = 0;
}
// src/app/enums/ToDoEventType.ts

export enum ToDoEventType {
    COMPLETE,
    DELETE
}

Nun haben wir ein "Gefäss" für unser Event, wir wissen, um was für einen Type (Complete oder Delete) und um welches To Do Item (anhand des Indexes) es sich handelt.

Ähnlich wie beim Input Bindung können wir auch hier eine Annotation nutzen:

// to-do-item.component.ts

@Output() toDoEvent = new EventEmitter<ToDoEvent>();

Die Klasse EventEmitter aus dem @angular/core Package erlaubt es uns, custom Event zu kreieren und diese zu feuern.

Nun müssen wir unsere ToDoItemComponent noch um drei Methode erweitern, welche unseren EventEmitter aufruft und jeweilige Event 'published'.

Zuerst implementieren wir die zwei Methoden, die direkt aus dem Template aufgerufen werden. Anschliessend schreiben wir eine private Methode für den emit des Events. Da diese nach Assen nicht sichtbar ist können (und wollen) wir diese nicht vom Template direkt aufrufen.

// to-do-item.component.ts

toggleCompleted(index:number){
    this.emitToDoEvent(index,ToDoEventType.COMPLETE);
}

deleteToDo(index:number){
    this.emitToDoEvent(index,ToDoEventType.DELETE);
}

private emitToDoEvent(index:number, toDoEventType:ToDoEventType){
    this.toDoEvent.emit({
        index: index,
        type:toDoEventType
    });
}

Zu guter letzt wollen wir das Event nun in unserer ToDosComponent entgegennehmen und entsprechen darauf reagieren:

<!-- to-dos.component.html -->

<app-to-do-item [toDo]="toDo" [i]="i" (toDoEvent)="handleToDoEvent($event)"></app-to-do-item>
//to-dos.component.ts

  handleToDoEvent(toDoEvent:ToDoEvent){
    if(toDoEvent.type === ToDoEventType.COMPLETE){
      this.toggleCompleted(toDoEvent.index);
    } else if (toDoEvent.type === ToDoEventType.DELETE){
      this.deleteToDo(toDoEvent.index);
    }
  }

Die Methode handleToDoEvent der ToDosComponent wird nun immer aufgerufen, wenn in der ToDoItemComponent ein Item erledigt oder gelöscht wird.

Zusammenfassend gesagt senden wir hier Daten von der Child zu der Parent Komponente.

Der letzte Schliff

Unsere Komponente ist nun voll funktional, leider sieht sie nicht mehr aus wie vorher. Grund dafür ist, dass sich das Markup geändert hat und die CSS Rules in der ToDosComponent nicht für die HTML Element aus der ToDoItemComponent gelten.

Im ersten Schritt wollen wir das div.todo aus der ToDosComponent entfernen bzw. direkt durch den ToDoItem Selektor ersetzen.

<!-- to-dos.component.html -->

<app-to-do-item class="todo" 
*ngFor="let toDo of toDos; let i = index" [toDo]="toDo" 
[i]="i" 
(toDoEvent)="handleToDoEvent($event)"></app-to-do-item>

Hier loopen wir nun auf einer Zeile durch unsere ToDos, geben dem Resultat die Klasse 'todo' und nutzen Input und Output Bindings, cool oder?

Nun stylen wir unsere ToDoItemComponent indem wir folgenden CSS Code einfügen:

/* to-do-item.component.css */

.index {
  flex: 1 1 50px;
}

.content {
  flex: 1 1 100%;
}

.done {
  text-decoration: line-through;
}

Zu guter Letzt fehlt nun noch das visuelle Feedback, ob ein Item bereits abgehakt ist, hier nutzen wir ein Data Binding auf ein HTML Property, namentlich das single class binding.

<!-- to-do-item.component.html

<div class="content" [class.done]="toDo.completed" (click)="toggleCompleted(i)">{{ toDo.content | superCoolPipe }}</div>

Angular erlaubt es uns, eine Boolean Expression direkt beim Definieren einer Klasse mitzugeben, so wird in das HTML Element class der Wert done nur geschrieben, wenn die Expression aus toDo.completed wahr ist.

Nun sieht unsere b-nova To Do Liste wieder schön aus und ist voll funktional! :-)

Nochmal langsam

Puh, das waren nun vielen Changes und neue Feature, wir wollen nochmal kurz rekapitulieren:

  • Mit Input nehmen wir Daten einer anderen Komponente entgegen
  • Mit Output senden wir eigene Events inklusive Daten nach Aussen

Visuell gesehen haben wir nun folgenden Aufbau:

Das komplette b-nova To Do List Projekt finden Sie auf GitHub.

Stay tuned!

Tom Trapp – Problemlöser, Innovator, Sportler. Am liebsten feilt Tom den ganzen Tag an der moderner Software und legt viel Wert auf objektiv sauberen, leanen Code.