Data Input
Die Kommunikation zwischen Komponenten in Angular ist ein grundlegender Aspekt. Vor kurzen wurde bei Angular die Art und Weise, wie Daten an Komponenten übergeben werden, überarbeitet und verbessert. Die neue Funktion input()
bietet einen eleganteren Ansatz für die Datenübergabe als die klassische @Input()
Dekorator-Syntax.
Die klassische Methode: @Input()
Dekorator
In der klassischen Syntax wurde der Input wie folgt umgesetzt.
import { Component, Input } from '@angular/core';
@Component({
selector: 'app-input-classic',
imports: [],
templateUrl: './input-classic.component.html',
styleUrl: './input-classic.component.scss'
})
export class InputClassicComponent {
@Input() inputValue: string = '';
}
<p>Übergebener Inhalt: <strong>{{ inputValue ? inputValue : '---' }}</strong></p>
<app-input-classic [inputValue]="'Input classic'"></app-input-classic>
Diese Syntax war lange Zeit bei Angular die primäre Methode Daten in ein Component zu übergeben. Sie hat einige Nachteile, wie die Notwendigkeit von Eigenschaftsdeklarationen und seprater Initialisierung.
Die neue Methode: input()
Funktion
In Angular wurde eine neue Methode eingeführt, die input()
Funktion. Diese Funktion bietet einen modernen Ansatz für die Übergabe der Daten in ein Component.
Grundlegende Syntax
name = input<string>();
name = input<string>('');
Vorteile
Die neue input()
Funktion bietet mehrere Vorteile.
- Weniger Boilerplate-Code: Die Deklaration und Initialisierung erfolgen in einem Schritt.
- Verbesserte Lesbarkeit: Der Code wird kompakter und klarer.
- Einfachere Refaktorierung: Eingabeeigenschaften können leichter umbenannt oder angepasst werden.
Hinweis: Der Input-Wert ist bei der modernen Methode eine Funktion. Daher muss diese aufgerufen werden, um den Wert zu erhalten.
Die Funktion gibt ein InputSignal
zurück.
Verwendung
import { Component, input } from '@angular/core';
@Component({
selector: 'app-input-modern',
imports: [],
templateUrl: './input-modern.component.html',
styleUrl: './input-modern.component.scss'
})
export class InputModernComponent {
inputValue = input<string>('');
}
<p>Übergebener Inhalt: <strong>{{ inputValue() }}</strong></p>
Die Übergabe im anderen Component ist gleich geblieben.
<app-input-modern [inputValue]="'Input modern'"></app-input-modern>
Im weiteren Verlauf wird der moderne Ansatz verwendet.
Pflicht Input-Daten
Man kann ein Input deklarieren, bei dem Daten-Übergabe pflicht ist. Um das zu erreichen, soll bei der Deklaration input.required
statt nur input
verwendet werden.
import { Component, input } from '@angular/core';
@Component({
selector: 'app-input-required',
imports: [],
templateUrl: './input-required.component.html',
styleUrl: './input-required.component.scss'
})
export class InputRequiredComponent {
requiredNumber = input.required<number>();
}
Im Template erfolg dann die klassische Verwendung.
<p>Die Nummer ist: {{ requiredNumber() }}</p>
<h3>Pflicht Daten-Übergabe</h3>
<app-input-required [requiredNumber]="42"></app-input-required>
Wenn man dann dieses Component ohne Daten-Übergabe verwendet, wirft Angular einen Fehler.
Erst, wenn man die entsprechenden Daten, auch vom definierten Typ, übergibt, wird alles korrekt bearbeitet.
Daten Transformation
Es besteht die Möglichkeit mit der neuen Art, die übergebenen Daten bequem zu transformieren. Für diesen Zweck akzeptiert die input()
Funktion ein Konfigurationsobjekt.
Beispiel - Objekte transformieren
Folgende Daten sollen beispielsweise in ein Component übergeben werden und diese sollen dort bereinigt werden. Es soll nämlich das Gehalt salary
von den Personen-Objekten (persons
) entfernt werden.
persons: {
name: string;
age: number;
salary: number;
}[] = [
{ name: 'John', age: 30, salary: 74000 },
{ name: 'Tom', age: 40, salary: 85000 },
{ name: 'Alice', age: 34, salary: 70000 }
]
In unserem Component, in welchen wir diese Daten übergeben und dort transformieren werden, müssen wir ein paar Sachen zusätzlich definieren.
Unsere Aufgabe ist es von diesen Personen-Objekten die Gehälter zu entfernen.
Auch, wenn man es an dieser Stelle die Typ-Definition direkt an der Variable vornehmen könnte, definiere ich hier explizit zwei Interfaces.
Da wir zwei unterschiedliche Datentypen (Personen-Objekte) haben, einmal mit und einmal ohne Gehalt, brauchen wir zwei Interfaces.
interface IPerson {
name: string;
age: number;
salary: number;
}
interface IPersonCleared {
name: string;
age: number;
}
Übergeben werden die Daten, wie auch in anderen Fällen, wie bereits bekannt.
<h3>Daten Transformation</h3>
<app-input-transform [persons]="persons"></app-input-transform>
Unsere input()
Funktion benötigt diesmal zwei Typ-Angaben.
- Modifizierte Daten: Das sind die Daten, die nach der Modifikation in die entsprechende Variable, in unserem Fall
persons
hineingeschrieben werden. - Eingabe-Daten: Das sind die unbereinigten Daten, die in dieses Component hineingegeben werden.
Außerdem teilen wir im Konfigurationsobjekt über die Eigenschaft transform mit, welche Funktion für die Transformation zuständig ist.
Hinweis: Für eine bessere Lesbarkeit wird hier JsonPipe verwendet, um ein Objekt im Template auszugeben.
import { JsonPipe } from '@angular/common';
import { Component, input } from '@angular/core';
interface IPerson {
name: string;
age: number;
salary: number;
}
interface IPersonCleared {
name: string;
age: number;
}
@Component({
selector: 'app-input-transform',
imports: [JsonPipe],
templateUrl: './input-transform.component.html',
styleUrl: './input-transform.component.scss'
})
export class InputTransformComponent {
persons = input<IPersonCleared[], IPerson[]>([], {
transform: clearPersons
});
}
function clearPersons(persons: IPerson[]): IPersonCleared[] {
const clearedPersons: IPersonCleared[] = persons.map(person => ({ name: person.name, age: person.age }));
return clearedPersons;
}
Es wichtig zu beachten, dass zuerst der Typ der Daten angegeben wird, welcher nach der Transformation erwartet wird und danach der Typ der Daten, die hineingegeben werden.
In der clearPersons
Funktion nehmen wir die unbereinigten Daten an, laufen über alle Datesätze drüber und entfernen jeweils das Feld salary
, indem wir einfach in der map
Funktion jeweils ein Objekt ohne salary
zurückgeben.
Ausgegeben werden die bereinigten Daten im Template wie folgt.
<div class="person_list">
<p>{{ persons() | json }}</p>
</div>
Beispiel - Unterschiedliche Datentypen
Wie bereits im oberen Beispiel der Fall zeigt, kann man am Eingang einen unterschiedlichen Datentyp haben, als der, welchen man nach der Transformation erhält.
In diesem, einfachen Beispiel soll es nochmals klarer werden.
@Component({ ... })
export class RectangleComponent {
widthPx = input('', { transform: appendPixel });
heightPx = input('', { transform: appendPixel });
}
function appendPixel(value: number): string {
return `${value}px`;
}
In diesem Fall wurde der Typ nicht explizit angegeben, was man aber machen kann. In diesem Fall würden die Typ-Angaben wie folgt aussehen.
widthPx = input<string, number>('', { transform: appendPixel });
heightPx = input<string, number>('', { transform: appendPixel });
Wir haben für dieses Beispiel folgende Daten, die übergeben werden.
rectangles: { id: number, width: number, height: number }[] = [
{ id: 1, width: 120, height: 230 },
{ id: 2, width: 90, height: 160 }
];
<div class="example_box">
<h3>Rechtecke</h3>
@for (r of rectangles; track r.id) {
<app-rectangle [widthPx]="r.width" [heightPx]="r.height"></app-rectangle>
}
</div>
In die Felder widthPx
und heightPx
werden jeweils Daten vom Typ number
übergeben.
In unserem RectangleComponent konvertieren wir die Eingaben in einen String und geben diese im Template aus.
<p>
Breite: {{ widthPx() }}<br>
Höhe: {{ heightPx() }}
</p>
Eingebaute Transformation
In Angular gibt es Mechanismen, um Eingabewert (Inputs) in einem Component automatisch in den gewünschten Datentyp zu konvertieren - so wie es native HTML-Attribute bzw. Werte, die von außen als Zeichenketten hereinkommen, erwarten.
Normalerweise bekommt ein Angular-Komponenteneingabewert (Input) entweder über Attribut-Bindings (z.B. <custom-slider disabled></custom-slider>
) oder über Property-Bindings (z.B. [disabled]="myDisabledValue"
). Da HTML-Attribute immer als String ankommen, kann es vorkommen, dass beispielsweise ein “false” als String hereinkommt, was in reinem HTML dazu führen würde, dass das Vorhandensein des Attributes zu einem “true” Wert interpretiert wird - denn in HTML ist bloße Existenz eines booleschen Attributs (wie disabled
) in der Regel ausreichend, um es zu aktivieren.
Angular löst dieses Problem, indem es eine Transformation vornimmt. Mit Hilfe der input
Funktion in Kombination mit den Transformationsoptionen wird der übergebene Wert automatisch umgewandelt.
booleanAttribute
Die Funktion booleanAttribute
transformiert den eingegebenen Wert in einen Boolean. Dabei wird das Verhalten von HTML-Boolean-Attributen nachgebildet.
Verhalten in HTML
Ein Boolean-Attribut wie disabled
wird in HTML allein durch seine Existenz als true
interpretiert, selbst wenn der Wert explizit als false
geschrieben wird.
Verhalten Angular
Mit booleanAttribute
wird dieser Fall behandelt:
- Wird ein Wert übergeben, der “false” als String ist, so wird dies explizit in
false
umgewandelt. - In allen anderen Fällen wird geprüft, ob ein “truthy” Wert vorliegt.
Für ein besseres Verständnis wird im folgenden Beispiel ein Component CustomCheckbox
erstellt, das ein paar Input-Eingaben hat.
import { Component, input, booleanAttribute, OnInit } from '@angular/core';
@Component({
selector: 'app-custom-checkbox',
imports: [],
templateUrl: './custom-checkbox.component.html',
styleUrl: './custom-checkbox.component.scss'
})
export class CustomCheckboxComponent implements OnInit {
private static checkboxCounter = 0;
public uniqueId!: string;
disabled = input(false, { transform: booleanAttribute });
checked = input(false, { transform: booleanAttribute });
label = input<string>('Checkbox');
ngOnInit(): void {
this.uniqueId = `id_${CustomCheckboxComponent.checkboxCounter++}`;
}
}
Hinweis: Die uniqueId
wird hier benötigt, damit jede Instanz (jedes neu erstellt Component) eine eindeutige ID hat, welche vom Label verwendet wird.
Im übergreifenden Component werden nun mehrere Instanzen von CustomCheckbox
mit verschiedenen Input-Konfigurationen erstellt.
<div class="example_box">
<h3>booleanAttribute</h3>
<app-custom-checkbox
disabled
label="Checkbox 1"
></app-custom-checkbox>
<hr>
<app-custom-checkbox
disabled="false"
label="Checkbox 2"
></app-custom-checkbox>
<hr>
<app-custom-checkbox
disabled="disabled"
label="Checkbox 3"
></app-custom-checkbox>
<hr>
<app-custom-checkbox
checked=""
label="Checkbox 4"
></app-custom-checkbox>
<hr>
<app-custom-checkbox
[disabled]="false"
label="Checkbox 5"
></app-custom-checkbox>
</div>
Das Ergebnis dieser Inputs an diesen Component-Instanzen sieht wie folgt aus.
Model Inputs
Model Inputs sind eine spezielle Art von Inputs, die nicht nur Daten empfangen, sondern auch Änderungen an diesen Daten zurück an die Elternkomponente melden können. Sie implementieren bidirektionales Datenfluss-Modell.
Beispiel - Einfache Bindung
Im folgenden Beispiel wird eine einfache Bindung von einem Wert gezeigt, welcher als Input hineingegeben, aber auch als Output-Wert in der Eltern-Komponente verwendet wird.
import { Component, model } from '@angular/core';
@Component({
selector: 'app-model-input',
standalone: true,
imports: [],
templateUrl: './model-input.component.html',
styleUrl: './model-input.component.scss'
})
export class ModelInputComponent {
count = model(0);
increment() {
this.count.update(value => value + 1);
}
decrement() {
this.count.update(value => value - 1);
}
}
Aktueller Wert: {{ count() }}
<hr>
<button (click)="increment()">Plus</button>
<button (click)="decrement()">Minus</button>
In der Eltern-Komponente gibt es eine eigene Variable, an die der Wert aus der Kind-Komponente gebunden wird.
Immer, wenn sich der Wert im Kind-Component aktualisiert wird, ändert sich auch der Wert der Variable, an die es per Two-Way-Bindung gebunden wurde.
In der Eltern-Komponente wird das Model Input wie folgt gebunden.
Hier ist eine Variable im Parent-Component, an die gebunden wird.
@Component({ ... })
export class ParentComponent {
currentModelCounter = 0;
}
Und so wird diese Variable currentModelCounter
im Template und an den Input des Kind-Components gebunden.
<app-model-input [(count)]="currentModelCounter"></app-model-input>