Startseite / Artikel / Angular-Komponenten von Grund auf: Selektoren, Templates, Eingaben, Ausgaben

Angular-Komponenten von Grund auf: Selektoren, Templates, Eingaben, Ausgaben

Eine praktische Übersicht über moderne, eigenständige Angular-Komponenten: Dateistruktur, Dekoratoren, Selektoren, Templates, begrenzte Styles sowie der Datenfluss zwischen Eltern- und Kindkomponenten mithilfe von input() und output().

2504 Wörter

Sobald ein Angular-Projekt erstellt und in Betrieb ist, stellt sich die nächste Frage: Wie wird der Bildschirm selbst erstellt? Die Antwort sind Komponenten – kleine, eigenständige Einheiten, die jeweils einen Teil der Benutzeroberfläche verwalten. In diesem Leitfaden wird erläutert, aus was eine Komponente besteht, wie eigenständige Komponenten im Vergleich zu älterem Code auf Basis von NgModule die Situation verändern, sowie wie Eltern- und Kindskomponenten mithilfe der aktuellen input()- und output()-APIs Daten und Ereignisse austauschen. Am Ende sollten Sie in der Lage sein, eine Komponente zu erstellen, sie in eine Seite einzubinden und Informationen in beide Richtungen zu übertragen.

Falls Sie weiterhin ein Projekt zum Ausprobieren benötigen, behandelt der Leitfaden zu der Einrichtung eines modernen Angular 22-Projekts die Einrichtung sowie die Ordnerstruktur, von der in diesem Artikel ausgegangen wird.

Warum Angular die Benutzeroberfläche in Komponenten aufteilt

Stellen Sie sich eine gewöhnliche Webseite vor: eine Navigationsleiste oben, ein Seitenbereich daneben, Inhalte zu Produkten in der Mitte und ein Fußbereich darunter. All diese Markup-Elemente, Styles und Verhaltensweisen könnten in einer einzigen riesigen Datei untergebracht werden, doch dann würde sie schnell unleserlich und noch schwieriger zu ändern. Angular ermutigt stattdessen dazu, die Seite in einzelne Teile aufzuteilen, wobei jeder Teil nur eine einzige Aufgabe hat.

Angular Application
│
├── Navbar Component
├── Sidebar Component
├── Product Component
├── Login Component
└── Footer Component

Eine Komponente enthält in der Regel vier Elemente: eine TypeScript-Klasse, die den Zustand und das Verhalten bereitstellt, ein HTML-Template zur Beschreibung der Struktur, Styles für das Erscheinungsbild sowie Metadaten, die Angular mitteilen, wie mit der Komponente umzugehen ist. Da jedes Element eine klare Aufgabe hat, kann man sich auf die Navbar konzentrieren, ohne an den Footer zu denken, dieselbe Produktkarte an mehreren Stellen wiederverwenden und Teile der Benutzeroberfläche getrennt voneinander testen. Ein Anmeldeformular, ein Profilpanel oder ein Dashboard-Widget sind alle geeignete Beispiele dafür.

Standalone-Komponenten sind Standard

Viele Angular-Tutorials, insbesondere die älteren, organisieren Komponenten innerhalb eines NgModule, der die Komponenten deklariert und auflistet, worauf sie angewiesen sind. Moderne Versionen von Angular haben sich von diesem Modell entfernt. Komponenten sind nun standardmäßig eigenständig, sodass eine Komponente ohne Registrierung in einem Modul funktionieren kann. Eine minimale Komponente sieht so aus:

import { Component } from '@angular/core';
@Component({
  selector: 'app-welcome',
  templateUrl: './welcome.html',
  styleUrl: './welcome.css'
})
export class WelcomeComponent {
}

Man könnte erwarten, ein Flag zu sehen, das die Komponente als eigenständig kennzeichnet, wie in der folgenden Zeile, doch das ist nicht mehr erforderlich:

standalone: true

Das Auslassen hat denselben Effekt, da „standalone“ das Standardverhalten ist. Wenn ein eigenständiges Komponente eine weitere Komponente, Direktive oder Pipe benötigt, listet es diese Abhängigkeit im imports-Array seiner eigenen @Component-Konfiguration auf. Der Vorteil ist die Lokalität: Man kann eine einzige Datei öffnen und genau erkennen, worauf die Komponente angewiesen ist, anstatt in einer Moduldefinition anderswo im Projekt zu suchen.

Wie eine Komponente auf dem Datenträger strukturiert ist

Eine typische Komponente befindet sich in ihrer eigenen Ordnerstruktur mit drei nebeneinander liegenden Dateien:

welcome/
│
├── welcome.ts
├── welcome.html
└── welcome.css

Die .ts-Datei enthält die Logik, die .html-Datei den Markup-Code und die .css-Datei die Styles. Hier ist, was jede dieser Dateien für eine einfache Begrüßungskomponente enthalten könnte. Zuerst die Klasse, die eine title-Eigenschaft für das Template bereitstellt:

import { Component } from '@angular/core';
@Component({
  selector: 'app-welcome',
  templateUrl: './welcome.html',
  styleUrl: './welcome.css'
})
export class WelcomeComponent {
  title = 'Welcome to Angular 22';
}

Das Template liest diese Eigenschaft mit doppelten geschweiften Klammern:

<h1>{{ title }}</h1>
<p>
  This is my first Angular component.
</p>

Und die Stylesheet-Datei bezieht sich nur auf Elemente in diesem Template:

h1 {
  color: #dd0031;
}
p {
  font-size: 18px;
}

Der @Component()-Decorator ist der Verbindungsstück. Er weist Angular auf das Template und die Stylesheet-Datei hin, sodass die drei Dateien wie eine Einheit funktionieren, wie im folgenden Überblick dargestellt:

welcome.ts
    │
    ├── Logic
    │
    ├── welcome.html
    │      ↓
    │    UI
    │
    └── welcome.css
           ↓
         Style

Angulos Stilrichtlinien empfehlen, eng miteinander verbundene Dateien wie diese im selben Verzeichnis zu halten – genau das tut auch diese Struktur. Wenn eine Komponente wächst, weiß man genau, wo sich ihre Markup-Dateien, Styles und Logik befinden.

Was der @Component()-Decorator Angular mitteilt

Der Decorator fügt der Klasse Metadaten hinzu. Über diese Metadaten lernt Angular, wofür die Klasse dient und wie sie dargestellt werden soll:

@Component({
  selector: 'app-welcome',
  templateUrl: './welcome.html',
  styleUrl: './welcome.css'
})

Drei Eigenschaften übernehmen hier die meiste Arbeit:

  • selector definiert den Tagnamen, den Sie verwenden, um das Komponente in anderen Templates einzubetten.
  • templateUrl verweist auf die HTML-Datei mit der Markup-Struktur der Komponente.
  • styleUrl verweist auf die Stylesheet-Datei der Komponente.

Es gibt weitere Optionen, wie beispielsweise imports zur Einbeziehung anderer Komponenten oder Angular-Funktionen, sowie inline-Alternative zu templateUrl und styleUrl, die später in diesem Artikel vorgestellt werden. Es ist nicht notwendig, sie alle im Voraus zu lernen; Sie werden vertraut, je mehr Komponenten Sie entwickeln.

Selektoren verwandeln eine Klasse in ein benutzerdefiniertes Element

Der Selektor ist der Identifikator, nach dem Angular in Templates sucht. In dieser Konfiguration ist er auf app-welcome gesetzt:

@Component({
  selector: 'app-welcome',
  ...
})

Daher ist der Selektorwert selbst einfach:

app-welcome

Wo auch immer Sie möchten, dass das Komponente erscheint, geben Sie diesen Namen ein, als wäre es eine HTML-Tag:

<app-welcome></app-welcome>

Wenn Angular beim Renderen eines Templates auf <app-welcome> stößt, erstellt es eine Instanz der entsprechenden Komponente und rendernt ihr Template innerhalb dieses Elements. Das Element, das zum Selektor passt, wird als Host-Element der Komponente bezeichnet und bleibt im DOM – was später wichtig ist, wenn Sie die Komponente als Ganzes stylen oder ihr Verhalten hinzufügen möchten.

Die Angular CLI erzeugt Selektoren mit dem für Ihre Anwendung konfigurierten Präfix, in der Regel app-, obwohl Sie beim Erstellen einer Komponente einen anderen Selektor wählen können. Das Präfix dient nicht nur ästhetischen Zwecken: Die Namen von Custom Elements sollten einen Bindestrich enthalten, und ein projektbezogenes Präfix verhindert, dass Ihre Komponenten mit nativen Elementen oder Tags aus Drittanbieter-Bibliotheken kollidieren.

Templates verbinden Markup mit Komponentendaten

Das Template definiert, was der Benutzer tatsächlich sieht. Im einfachsten Fall handelt es sich dabei um reines HTML:

<h1>Welcome!</h1>
<p>We are learning Angular components.</p>

Was Angular-Templates leistungsfähiger als statisches HTML macht, ist die Fähigkeit, Daten zu lesen und auf im Komponentenklassen definiertes Verhalten zuzugreifen. Angenommen, die Klasse deklariert eine Eigenschaft:

name = 'Mubbassir';

Dann kann das Template diese über Interpolation anzeigen:

<h1>Hello {{ name }}!</h1>

Angular hält die dargestellte Ausgabe in Einklang mit den Daten: Wenn sich der gebundene Wert ändert, wird die Seite aktualisiert, ohne dass man selbst den DOM anfassen muss. Diese Verbindung zwischen der TypeScript-Klasse und ihrem Template ist die Grundlage für alles andere in Angular – von der Ereignisverarbeitung bis hin zu Formularen.

Komponentenstile bleiben standardmäßig eingegrenzt

Jede Komponente kann ihre eigenen Styles haben:

.card {
  padding: 20px;
  border-radius: 10px;
}
h2 {
  color: #dd0031;
}

Standardmäßig wendet Angular eine emulierte Ansichtsverkapselung auf diese Regeln an. Während der Kompilierung überschreibt es die Selektoren und fügt generierte Attribute zu den Elementen der Komponente hinzu, sodass eine Regel wie h2 { ... } nur die h2-Elemente im Template dieser Komponente erfasst und nicht alle Überschriften in der Anwendung. In einer Anwendung mit Hunderten von Komponenten verhindert diese Schutzmaßnahme, dass eine Änderung eines Styles an einer Stelle unbemerkt das Erscheinungsbild einer anderen beeinträchtigt.

Die Kapselung schließt gemeinsames Styling nicht aus. Regeln, die überall gelten sollten – wie Typografie, Resets oder Layout-Utilities – gehören in die globale Stylesheet der Anwendung, während die Komponentendateien die dem jeweiligen Komponenten spezifischen Styles enthalten.

Eine Komponente mit dem Angular CLI erstellen

Das Manuell-Erstellen der Dateien funktioniert auch, doch der CLI erledigt dies schneller und mit konsistenten Namenskonventionen. Von der Projektwurzel aus führen Sie Folgendes aus:

ng generate component user-card

oder die abgekürzte Form:

ng g c user-card

Der Generator erzeugt ein Verzeichnis wie dieses (der zusätzliche = nach dem Dateinamen der Spezifikation ist ein Tippfehler in der Auflistung und nicht Teil des Namens):

user-card/
├── user-card.ts
├── user-card.html
├── user-card.css
└── user-card.spec.ts=

Der neuere Dateinamensstil ist kürzer als die ältere Konvention user-card.component.ts. Ob eine .spec.ts-Testdatei erstellt wird, hängt von der Testkonfiguration des Projekts ab. Neuere CLI-Versionen, die dem aktualisierten Stilguide folgen, neigen außerdem dazu, Klassennamen ohne das Suffix Component zu erzeugen (zum Beispiel UserCard); die untenstehenden Beispiele behalten das Suffix bei – beide Namensformen funktionieren, solange die Importe übereinstimmen.

Wie Eltern- und Kindkomponenten miteinander kommunizieren

Echte Benutzeroberflächen bestehen aus Komponentenbäumen, und diese müssen Informationen austauschen. Betrachten wir eine Elternkomponente, die über einen Benutzer Bescheid weiß, und eine Kindkomponente, deren Aufgabe es ist, diesen Benutzer anzuzeigen. Die Elternkomponente benötigt eine Möglichkeit, Daten weiterzuleiten, und die Kindkomponente muss einen Weg haben, zurückzumelden, wenn etwas geschieht – beispielsweise bei einem Klick auf einen Button.

Angular kümmert sich bei der nach unten gerichteten Kommunikation um die Eingaben und bei der nach oben gerichteten Kommunikation um die Ausgaben:

Parent Component
      │
      │  Data
      │
      ▼
Child Component
      │
      │  Event
      │
      ▼
Parent Component

Für neues Code empfiehlt Angular die auf Signalen basierenden Funktionen input() und output(). Die auf Dekoratoren basierenden APIs @Input() und @Output() werden weiterhin vollständig unterstützt, sodass man sie auch in bestehenden Codebasen noch antreffen wird.

Daten nach unten übergeben mit input()

Beginnen Sie mit der Klasse des Kindkomponenten:

import { Component, input } from '@angular/core';
@Component({
  selector: 'app-user-card',
  templateUrl: './user-card.html',
  styleUrl: './user-card.css'
})
export class UserCardComponent {
  name = input.required<string>();
}

Die wichtige Zeile ist die Deklaration der Eingabe:

name = input.required<string>();

Dies gibt an, dass UserCardComponent von jedem, der es verwendet, einen name-String erwartet. Durch die Verwendung von input.required() wird diese Erwartung strenger: Wenn ein Elternteil vergisst, name zu binden, meldet Angular einen Fehler anstelle davon, stillschweigend einen leeren Wert anzuzeigen. Für optionale Eingabefelder akzeptiert input() stattdessen einen Standardwert.

Weil name ein Signal und nicht eine einfache Eigenschaft ist, wird es durch Aufruf gelesen. In der Klasse schreibt man:

this.name()

und im Template:

{{ name() }}

Das Vergessen der Klammern ist ein häufiger Anfängerfehler; ohne sie zeigt das Template die Signalfunktion selbst anstelle ihres Wertes. Das Template des Kindkomponenten verwendet die Eingabe so:

<div class="card">
  <h2>{{ name() }}</h2>
  <p>Welcome to Angular!</p>
</div>

Auf der Seite des Elternteils wird beim Einbetten des Kindkomponenten ein Wert an die Eingabe gebunden:

<app-user-card [name]="userName" />

Falls die Elternklasse Folgendes enthält:

userName = 'Yuvaraj';

erhält die Kindklasse diesen String und rendernt ihn:

Yuvaraj

Die eckigen Klammern sorgen dafür, dass es sich um eine Eigenschaftsbindung handelt:

[name]="userName"

Sie weisen Angular an, userName als Ausdruck in der Elternklasse auszuwerten und das Ergebnis an die name-Eingabe der Kindklasse weiterzuleiten. Ohne die Klammern würde Angular stattdessen den literalen Text userName übergeben. Da es sich um ein Signal handelt, wird die Kindklasse automatisch neu gerendert, sobald sich der Wert der Elternklasse ändert.

Ereignisse mit output() nach oben senden

Geben Sie der Kindklasse nun einen Button und lassen Sie die Elternklasse erkennen, wann ein Benutzer ausgewählt wird. Die Kindklasse deklariert eine Ausgabe und eine Methode, die darüber Ereignisse aussendet:

import { Component, input, output } from '@angular/core';
@Component({
  selector: 'app-user-card',
  templateUrl: './user-card.html',
  styleUrl: './user-card.css'
})
export class UserCardComponent {
  name = input.required<string>();
  selected = output<string>();
  selectUser() {
    this.selected.emit(this.name());
  }
}

Durch die Deklaration der Ausgabe wird ein benutzerdefiniertes Ereignis definiert, das einen String enthält:

selected = output<string>();

Der Aufruf von emit löst dieses Event aus und übermittelt den aktuellen Namen als Payload:

this.selected.emit(this.name());

Das Template des Kindkomponenten ruft selectUser() auf, wenn auf die Schaltfläche geklickt wird. Beachten Sie die (click)-Syntax, welche Angular’s Event-Bindung darstellt:

<div class="card">
 <h2>{{ name() }}</h2>
  <button (click)="selectUser()">
    Select User
  </button>
</div>

Die Elternkomponente lauscht dem benutzerdefinierten Event mithilfe derselben Klammern-Syntax, diesmal mit dem Namen der Ausgabe:

<app-user-card
  [name]="userName"
  (selected)="userSelected($event)"
/>

Klammern bedeuten „auf dieses Event lauschen“, und $event enthält den Wert, den das Kindkomponenten ausgesendet hat – hier den Namen des Benutzers. Die Elternkomponente handelt diesen Wert in einer normalen Methode ab:

userSelected(name: string) {
  console.log('Selected user:', name);
}

Diese Aufgabenteilung sorgt dafür, dass die Kindkomponente wiederverwendbar bleibt. Sie weiß nicht und kümmert sich auch nicht darum, was die Elternkomponente mit einer Auswahl macht; sie gibt lediglich an, dass eine Auswahl stattgefunden hat. Die Elternkomponente entscheidet, ob sie diese protokolliert, navigiert oder ihren eigenen Zustand aktualisiert.

Ein Element auf die Seite setzen

Das abschließende Beispiel verbindet alle Komponenten miteinander. Diese Version der Begrüßungskomponente verwendet inline template- und styles-Angaben im Dekorator anstelle separater Dateien, was bei sehr kleinen Komponenten praktisch ist:

import { Component } from '@angular/core';

@Component({
  selector: 'app-welcome',
  template: `
    <div class="welcome-card">
      <h2>Welcome Component</h2>
      <p>This component was created using the <strong>app-welcome</strong> selector.</p>
    </div>
  `,
  styles: `
    .welcome-card {
      padding: 25px;
      margin-top: 20px;
      border-radius: 12px;
      background: #f5f5f5;
      text-align: center;
      font-family: Arial, sans-serif;
    }
    h2 {
      color: #dd0031;
    }
    p {
      font-size: 18px;
    }
  `
})
export class WelcomeComponent {}

Die Wurzelkomponente importiert WelcomeComponent in ihrem imports-Array, wodurch der app-welcome-Selektor in ihrem Template verwendet werden kann. Wenn dieser Import weggelassen wird, ist das eine der häufigsten Ursachen für den Fehler „is not a known element“ in eigenständigen Anwendungen:

import { Component } from '@angular/core';
import { WelcomeComponent } from './welcome';

@Component({
  selector: 'app-root',
  imports: [WelcomeComponent],
  templateUrl: './app.html',
  styleUrl: './app.css'
})
export class App {}

Zum Schluss platziert das Wurzeltemplate die Komponente mithilfe ihres Selektors:

<div class="container">
<h1>Angular Components</h1>
  <p>
    Below is our custom Angular component:
  </p>
  <app-welcome></app-welcome>
</div>

Inline-Templates halten alles in einer Datei zusammen, werden aber unpraktisch, sobald die Markup-Struktur mehrere Zeilen umfasst. Eine sinnvolle Regel ist, bei kleinen Präsentationsteilen mit Inline-Templates zu beginnen und sobald das Template eine echte Struktur benötigt, auf getrennte .html- und .css-Dateien umzusteigen.

Hauptpunkte

  • Eine Komponente kombiniert eine TypeScript-Klasse, ein Template sowie optionalen Stilcode, die durch den @Component()-Decorator miteinander verbunden sind.
  • In modernem Angular ist „Standalone“ der Standard; Abhängigkeiten werden in dem imports-Array jeder Komponente deklariert, anstatt in einem NgModule.
  • Der Selector ist das benutzerdefinierte Tag, mit dem eine Komponente platziert wird, und das entsprechende Element dient als Host für diese Komponente.
  • Templates binden an Daten der Klasse, und Angular aktualisiert die Ansicht, wenn sich diese Daten ändern.
  • Die Styles der Komponenten werden durch emulierte Inkapselung abgegrenzt, während die für die gesamte Anwendung geltenden Regeln in der globalen Stylesheet-Datei enthalten sind.
  • Verwenden Sie input() (oder input.required()), um Daten nach unten weiterzuleiten, und output(), um Ereignisse nach oben zu senden; lesen Sie Eingaben, indem Sie sie als Signale aufrufen.
  • So betrachtet ist eine Angular-Anwendung ein Baum aus Komponenten, wobei jede für ihren eigenen Teil der Benutzeroberfläche verantwortlich ist und über explizite Eingaben sowie Ausgaben miteinander kommuniziert. Genau diese Struktur sorgt dafür, dass große Angular-Anwendungen auch bei zunehmender Größe verständlich bleiben.