Kapitel 4. Angular-Komponenten verstehen und nutzen
Diese Arbeit wurde mithilfe von KI übersetzt. Wir freuen uns über dein Feedback und deine Kommentare: translation-feedback@oreilly.com
Im vorigen Kapitel haben wir uns mit den eingebauten Direktiven von Angular befasst, mit denen wir gängige Funktionen wie das Ein- und Ausblenden von Elementen, das Wiederholen von Templates usw. ausführen können. Wir haben mit Direktiven wie ngIf
und ngForOf
gearbeitet und ein Gefühl dafür bekommen, wie und wann wir sie einsetzen können.
In diesem Kapitel werden wir uns näher mit Komponenten beschäftigen, also mit den Elementen, die wir erstellen, um die Benutzeroberfläche darzustellen und die Interaktion der Benutzer mit den von uns erstellten Anwendungen zu ermöglichen. Wir befassen uns mit einigen nützlichen Attributen, die du bei der Erstellung von Komponenten angeben kannst, mit dem Lebenszyklus der Komponente und den verschiedenen Hooks, die Angular dir zur Verfügung stellt, und schließlich damit, wie du Daten in und aus deinen benutzerdefinierten Komponenten übermittelst. Am Ende des Kapitels solltest du in der Lage sein, die meisten gängigen Aufgaben im Zusammenhang mit Komponenten auszuführen und dabei zu verstehen, was du tust und warum.
Komponenten - eine Rekapitulation
Im vorherigen Kapitel haben wir gesehen, dass Angular nur Direktiven hat und dass Direktiven für mehrere Zwecke wiederverwendet werden. Wir haben uns mit Attribut- und Strukturdirektiven beschäftigt, die es uns ermöglichen, das Verhalten eines bestehenden Elements oder die Struktur des gerenderten Templates zu ändern.
Die dritte Art von Direktiven sind Komponenten, die wir bereits im ersten Kapitel verwendet haben. Bis zu einem gewissen Grad kannst du eine Angular-Anwendung als einen Baum von Komponenten betrachten. Jede Komponente hat ein bestimmtes Verhalten und ein Template, das gerendert wird. Diese Vorlage kann dann wiederum andere Komponenten verwenden und so einen Baum von Komponenten bilden, der die Angular-Anwendung darstellt, die im Browser gerendert wird.
Im einfachsten Fall ist eine Komponente nichts anderes als eine Klasse, die das Verhalten (welche Daten geladen werden sollen, welche Daten dargestellt werden sollen und wie auf Benutzerinteraktionen reagiert werden soll) und ein Template (wie die Daten dargestellt werden) kapselt. Es gibt jedoch mehrere Möglichkeiten, dies zu definieren, sowie weitere Optionen, die wir in den folgenden Abschnitten behandeln werden.
Definieren einer Komponente
Wir definieren eine Komponente mithilfe des TypeScript-Dekorators Component
. Damit können wir jede Klasse mit Metadaten versehen, die Angular mitteilen, wie die Komponente funktioniert, was gerendert werden soll und so weiter. Werfen wir noch einmal einen Blick auf die stock-item
Komponente, die wir erstellt haben, um zu sehen, wie eine einfache Komponente aussehen würde:
@
Component
({
selector
:
'app-stock-item'
,
templateUrl
:
'./stock-item.component.html'
,
styleUrls
:
[
'./stock-item.component.css'
]
})
export
class
StockItemComponent
implements
OnInit
{
// Code omitted here for clarity
}
Die einfachste Komponente benötigt nur einen Selektor (um Angular mitzuteilen, wie es Instanzen der verwendeten Komponente finden kann) und ein Template (das Angular rendern muss, wenn es das Element findet). Alle anderen Attribute im Component
Dekorator sind optional. Im vorangegangenen Beispiel haben wir festgelegt, dass StockItemComponent
gerendert werden soll, wenn Angular auf den Selektor app-stock-item
trifft, und dass die Datei stock-item.component.html gerendert werden soll, wenn es auf das Element trifft. Schauen wir uns die Attribute des Dekorators etwas genauer an.
Selektor
Mit dem selector-Attribut, das wir in Kapitel 2 kurz angesprochen haben, können wir festlegen, wie Angular die Komponente erkennt, wenn sie in HTML verwendet wird. Das selector-Attribut nimmt einen String-Wert an, der der CSS-Selektor ist, den Angular verwendet, um das Element zu identifizieren. Bei der Erstellung neuer Komponenten wird empfohlen, Elementselektoren zu verwenden (wie bei app-stock-item
), aber technisch gesehen kannst du auch jeden anderen Selektor verwenden. Hier sind zum Beispiel einige Möglichkeiten, wie du das Selektor-Attribut angeben kannst und wie du es im HTML verwenden würdest:
-
selector: 'app-stock-item'
würde dazu führen, dass die Komponente als<app-stock-item></app-stock-item>
im HTML verwendet wird. -
selector: '.app-stock-item'
würde dazu führen, dass die Komponente als CSS-Klasse wie<div class="app-stock-item"></div>
im HTML verwendet wird. -
selector: '[app-stock-item]'
würde dazu führen, dass die Komponente als Attribut für ein bestehendes Element wie<div app-stock-item></div>
in der HTML-Datei verwendet wird.
Du kannst den Selektor so einfach oder komplex gestalten, wie du willst. Als Faustregel gilt jedoch, dass du dich an einfache Elementselektoren halten solltest, es sei denn, du hast einen triftigen Grund, der dagegen spricht.
Template
Bisher haben wir templateUrl
verwendet, um die Vorlage zu definieren, die zusammen mit einer Komponente verwendet werden soll. Der Pfad, den du dem Attribut templateUrl
übergibst, ist relativ zum Pfad der Komponente. Im vorherigen Fall können wir die templateUrl
entweder als:
templateUrl: './stock.item.component.html'
oder:
templateUrl: 'stock.item.component.html'
und es würde funktionieren. Wenn du aber versuchst, eine absolute URL oder etwas anderes anzugeben, würde deine Kompilierung abbrechen. Interessant ist, dass die Anwendung, die Angular baut, im Gegensatz zu AngularJS (1.x) die Vorlage nicht per URL zur Laufzeit lädt. Stattdessen kompiliert Angular einen Build vor und sorgt dafür, dass die Vorlage als Teil des Build-Prozesses eingefügt wird.
Anstelle von templateUrl
können wir die Vorlage auch inline in der Komponente angeben, indem wir die Option template
verwenden. So kann die Komponente alle Informationen enthalten, anstatt sie auf HTML- und TypeScript-Code aufzuteilen.
Tipp
In einer Komponente kann nur eines von template
und templateUrl
angegeben werden. Du kannst nicht beide verwenden, aber mindestens eine ist notwendig.
Das hat keine Auswirkungen auf die fertige Anwendung, da Angular den Code in ein einziges Bundle kompiliert. Der einzige Grund, warum du deinen Template-Code in eine separate Datei aufteilen solltest, ist, um bessere IDE-Funktionen wie die Syntax-Vervollständigung und Ähnliches zu nutzen, die für Dateierweiterungen spezifisch sind. Generell solltest du deine Templates getrennt halten, wenn sie mehr als drei oder vier Zeilen lang sind oder eine gewisse Komplexität aufweisen.
Schauen wir uns an, wie unsere stock-item
Komponente mit einem Inline-Templating aussehen könnte:
import
{
Component
,
OnInit
}
from
'@angular/core'
;
import
{
Stock
}
from
'../../model/stock'
;
@
Component
({
selector
:
'app-stock-item'
,
template
:
`
<
div
class
=
"stock-container"
>
<
div
class
=
"name"
>
{{
stock
.
name
+
' ('
+
stock
.
code
+
')'
}}
<
/div>
<
div
class
=
"price"
[
class
]
=
"stock.isPositiveChange() ? 'positive' : 'negative'"
>
$
{{
stock
.
price
}}
<
/div>
<
button
(
click
)
=
"toggleFavorite($event)"
*
ngIf
=
"!stock.favorite"
>
Add
to
Favorite
<
/button>
<
/div>
`
,
styleUrls
:
[
'./stock-item.component.css'
]
})
export
class
StockItemComponent
implements
OnInit
{
// Code omitted here for clarity
}
Tipp
In ECMAScript 2015 (und TypeScript) können wir mehrzeilige Templates mit dem Symbol ` (Backtick) definieren, anstatt Strings mit dem Operator + (Plus) über mehrere Zeilen zu verketten. Das nutzen wir normalerweise, wenn wir Inline-Vorlagen definieren.
Den fertigen Code findest du im Ordner chapter4/component-template im GitHub-Repository.
Alles, was wir getan haben, ist, die Vorlage in das template
Attribut des Component
Dekorators zu verschieben. In diesem speziellen Fall würde ich jedoch empfehlen, die Vorlage nicht inline zu verschieben, da es mehr als ein paar Zeilen gibt, in denen eine gewisse Arbeit geleistet wird. Beachte, dass wir durch das Verschieben nach template
das vorherige Attribut templateUrl
entfernt haben.
Styles
Einer bestimmten Komponente können mehrere Stile zugeordnet werden. Auf diese Weise kannst du komponentenspezifisches CSS sowie potenziell jedes andere allgemeine CSS, das auf die Komponente angewendet werden muss, einbinden. Ähnlich wie bei Templates kannst du dein CSS entweder mit dem Attribut styles
einbinden oder, wenn es sich um eine große Menge an CSS handelt oder du deine IDE nutzen willst, kannst du es in eine separate Datei auslagern und mit dem Attribut styleUrls
in deine Komponente einbinden. In beiden Fällen wird ein Array als Eingabe verwendet.
Eine Sache, die Angular von Haus aus fördert, ist die vollständige Kapselung und Isolierung von Stilen. Das bedeutet, dass die Stile, die du in einer Komponente definierst und verwendest, standardmäßig keine anderen über- oder untergeordneten Komponenten beeinflussen. So kannst du sicher sein, dass die CSS-Klassen, die du in einer Komponente definierst, sich nicht unwissentlich auf eine andere Komponente auswirken, es sei denn, du ziehst die notwendigen Stile explizit ein.
Wie bei Templates werden auch hier die Stile nicht zur Laufzeit von Angular übernommen, sondern vorkompiliert und ein Bundle mit den erforderlichen Stilen erstellt. Die Entscheidung, ob du styles
oder styleUrls
verwendest, ist also eine persönliche Entscheidung, die keine großen Auswirkungen auf die Laufzeit hat.
Warnung
Verwende nicht sowohl styles
als auch styleUrls
zusammen. Angular wählt dann entweder das eine oder das andere aus, was zu unerwartetem Verhalten führt.
Schauen wir uns kurz an, wie die Komponente aussehen könnte, wenn wir die Stile einbetten:
import
{
Component
,
OnInit
}
from
'@angular/core'
;
import
{
Stock
}
from
'../../model/stock'
;
@
Component
({
selector
:
'app-stock-item'
,
templateUrl
:
'stock-item.component.html'
,
styles
:
[
`
.
stock
-
container
{
border
:1px
solid
black
;
border
-
radius
:5px
;
display
:inline
-
block
;
padding
:10px
;
}
.
positive
{
color
:green
;
}
.
negative
{
color
:red
;
}
`
]
})
export
class
StockItemComponent
implements
OnInit
{
// Code omitted here for clarity
}
Du findest den fertigen Code im Ordner chapter4/component-style im GitHub-Repository.
Du kannst natürlich auch mehrere Style-Strings an das Attribut übergeben. Die Entscheidung zwischen styles
und styleUrls
ist eine Frage der persönlichen Vorliebe und hat keinen Einfluss auf die endgültige Leistung der Anwendung.
Stil-Kapselung
Im vorangegangenen Abschnitt haben wir darüber gesprochen, wie Angular die Stile kapselt, um sicherzustellen, dass sie keine deiner anderen Komponenten verunreinigen. Du kannst Angular sogar mitteilen, ob es dies tun muss oder nicht, oder ob die Stile global zugänglich sein sollen. Dies kannst du mit dem encapsulation
Attribut auf dem Component
Dekorator festlegen. Das Attribut encapsulation
kann einen von drei Werten annehmen:
ViewEncapsulation.Emulated
-
Dies ist die Standardeinstellung, bei der Angular shimmed CSS erstellt, um das Verhalten zu emulieren, das shadow DOMs und shadow roots bieten.
ViewEncapsulation.Native
-
Im Idealfall verwendet Angular Shadow Roots. Das funktioniert nur auf Browsern und Plattformen, die das von Haus aus unterstützen.
ViewEncapsulation.None
-
Verwendet globales CSS, ohne jegliche Kapselung.
Was ist das Schatten-DOM?
HTML, CSS und JavaScript neigen standardmäßig dazu, im Kontext der aktuellen Seite global zu sein. Das bedeutet, dass eine ID, die einem Element zugewiesen wird, leicht mit einem anderen Element an anderer Stelle auf der Seite kollidieren kann. Ebenso kann eine CSS-Regel, die einer Schaltfläche in einer Ecke der Seite zugewiesen wurde, Auswirkungen auf eine andere Schaltfläche haben, die damit nichts zu tun hat.
Am Ende müssen wir uns spezielle Namenskonventionen ausdenken, CSS-Hacks wie !important
verwenden und viele weitere Techniken einsetzen, um dies im Entwicklungsalltag zu umgehen.
Shadow DOM behebt dieses Problem, indem es HTML DOM und CSS skaliert. Es bietet die Möglichkeit, das Styling auf eine Komponente zu beschränken (und damit zu verhindern, dass die Stile nach außen dringen und den Rest der Anwendung beeinträchtigen) und das DOM zu isolieren und in sich geschlossen zu halten.
Du kannst mehr darüber in der Dokumentation für eigenständige Webkomponenten nachlesen.
Der beste Weg, um zu sehen, wie sich dies auf unsere Anwendung auswirkt, ist, eine kleine Änderung vorzunehmen und zu sehen, wie sich unsere Anwendung unter verschiedenen Umständen verhält.
Fügen wir zunächst den folgenden Codeschnipsel in die Datei app.component.css ein. Wir verwenden die gleiche Basis wie im vorherigen Kapitel und der fertige Code ist im Ordner chapter4/component-style-encapsulation zu finden:
.name
{
font-size
:
50px
;
}
Wenn wir die Anwendung jetzt ausführen, hat das keine Auswirkungen auf unsere Anwendung. Versuchen wir nun, die Eigenschaft encapsulation
in der Hauptkomponente AppComponent
zu ändern. Wir werden die Komponente wie folgt ändern:
import
{
Component
,
ViewEncapsulation
}
from
'@angular/core'
;
@
Component
({
selector
:
'app-root'
,
templateUrl
:
'./app.component.html'
,
styleUrls
:
[
'./app.component.css'
],
encapsulation
:ViewEncapsulation.None
})
export
class
AppComponent
{
title
=
'app works!'
;
}
Wir haben die Zeile encapsulation: ViewEncapsulation.None
zu unserem Component
Dekorator hinzugefügt (natürlich, nachdem wir das ViewEncapsulation
enum aus Angular importiert haben). Wenn wir nun unsere Anwendung aktualisieren, wirst du sehen, dass der Name der Aktie auf 50px aufgebläht wurde. Das liegt daran, dass die Stile, die auf AppComponent
angewendet werden, nicht nur auf die Komponente beschränkt sind, sondern jetzt den globalen Namensraum einnehmen. Jedes Element, das die Klasse name
zu sich selbst hinzufügt, bekommt also diese Schriftgröße zugewiesen.
ViewEncapsulation.None
ist eine gute Möglichkeit, gemeinsame Stile auf alle untergeordneten Komponenten anzuwenden, birgt aber das Risiko, den globalen CSS-Namensraum zu verschmutzen und unbeabsichtigte Auswirkungen zu haben.
Andere
Es gibt noch viel mehr Attribute als die, die wir im Component
Dekorator behandelt haben. Wir werden hier kurz auf einige davon eingehen und die Diskussion über andere für spätere Kapitel aufheben, wenn sie wichtiger werden. Hier ist ein kurzer Überblick über einige der wichtigsten Attribute und ihre Verwendung:
- Leerzeichen entfernen
-
Mit Angular kannst du alle unnötigen Leerzeichen aus deinem Template entfernen (wie von Angular definiert, einschließlich mehr als ein Leerzeichen, Leerzeichen zwischen Elementen usw.). Dies kann helfen, die Größe der Vorlage zu reduzieren, indem dein HTML komprimiert wird. Du kannst diese Funktion (die standardmäßig auf
false
eingestellt ist) über daspreserveWhitespaces
Attribut der Komponente einstellen. Mehr über diese Funktion erfährst du in der offiziellen Dokumentation. - Animationen
-
Angular bietet dir mehrere Trigger, mit denen du jeden Teil der Komponente und ihren Lebenszyklus steuern und animieren kannst. Dafür gibt es eine eigene DSL, mit der Angular bei Zustandsänderungen innerhalb des Elements animieren kann.
- Interpolation
-
Es gibt Fälle, in denen die standardmäßigen Angular-Interpolationskennzeichen (die Doppelkurven
{{
und}}
) die Integration mit anderen Frameworks oder Technologien behindern. Für diese Fälle bietet Angular die Möglichkeit, die Interpolationskennungen auf Komponentenebene zu überschreiben, indem du die Start- und Endbegrenzer angibst. Dazu verwendest du das Attributinterpolation
, das ein Array mit zwei Strings, den Anfangs- und Endmarkern für die Interpolation, enthält. Standardmäßig lauten sie['{{', '}}']
, aber du kannst sie außer Kraft setzen, indem du z. B.interpolation: ['<<', '>>']
angibst, um die Interpolationssymbole für nur diese Komponente durch<<
und>>
zu ersetzen. - Anbieter ansehen
-
Mit View-Providern kannst du Provider definieren, die Klassen/Dienste in eine Komponente oder eines ihrer Kinder einfügen. Normalerweise brauchst du das nicht, aber wenn es bestimmte Komponenten gibt, bei denen du eine Klasse oder einen Dienst außer Kraft setzen oder ihre Verfügbarkeit einschränken willst, kannst du mit dem Attribut
viewProviders
eine Reihe von Anbietern für eine Komponente angeben. Wir werden dies in Kapitel 8 genauer behandeln. - Exportieren der Komponente
-
Bis jetzt haben wir die Funktionen der Komponentenklasse im Kontext der Vorlage verwendet. Es gibt jedoch Anwendungsfälle (vor allem, wenn wir uns mit Direktiven und komplexeren Komponenten befassen), in denen wir dem Benutzer der Komponente erlauben wollen, Funktionen der Komponente von außen aufzurufen. Ein Anwendungsfall könnte sein, dass wir eine Karussellkomponente anbieten, aber dem Benutzer der Komponente die Möglichkeit geben wollen, die nächste/vorherige Funktion zu steuern. In diesen Fällen können wir das Attribut
exportAs
desComponent
Dekorators verwenden. changeDetection
-
Standardmäßig prüft Angular jede Bindung in der Benutzeroberfläche, um zu sehen, ob es ein UI-Element aktualisieren muss, wenn sich ein Wert in unserer Komponente ändert. Das ist für die meisten Anwendungen akzeptabel, aber wenn unsere Anwendungen größer und komplexer werden, möchten wir vielleicht kontrollieren, wie und wann Angular die Benutzeroberfläche aktualisiert. Anstatt dass Angular entscheidet, wann die Benutzeroberfläche aktualisiert werden muss, möchten wir Angular explizit mitteilen, wann es die Benutzeroberfläche manuell aktualisieren muss. Dazu verwenden wir das Attribut
changeDetection
, bei dem wir den Standardwert vonChangeDetectionStrategy.Default
aufChangeDetectionStrategy.OnPush
überschreiben können. Das bedeutet, dass es nach dem ersten Rendering an uns liegt, Angular mitzuteilen, wenn sich der Wert ändert. Angular wird die Bindungen der Komponente nicht automatisch überprüfen. Wir werden das später in diesem Kapitel noch genauer erklären.
Es gibt noch viele weitere Attribute und Funktionen für Komponenten, die wir in diesem Kapitel nicht behandeln. Du solltest einen Blick in die offizielle Dokumentation für Komponenten werfen, um dich mit den weiteren Möglichkeiten vertraut zu machen, oder tiefer in die Details eintauchen.
Komponenten und Module
Bevor wir uns mit den Details des Lebenszyklus einer Komponente befassen, wollen wir kurz darauf eingehen, wie Komponenten mit Modulen verknüpft sind und in welchem Verhältnis sie zueinander stehen. In Kapitel 2 haben wir gesehen, dass wir bei der Erstellung einer neuen Komponente diese in ein Modul einbinden müssen. Wenn du eine neue Komponente erstellst und sie nicht zu einem Modul hinzufügst, beschwert sich Angular, dass du Komponenten hast, die nicht Teil eines Moduls sind.
Damit eine Komponente im Kontext eines Moduls verwendet werden kann, muss sie in deine Moduldeklarationsdatei importiert und im Array declarations
deklariert werden. Dadurch wird sichergestellt, dass die Komponente für alle anderen Komponenten innerhalb des Moduls sichtbar ist.
Es gibt drei spezifische Attribute auf NgModule
, die sich direkt auf die Komponenten und ihre Verwendung auswirken und die du unbedingt kennen solltest. Anfangs ist nur declarations
wichtig. Sobald du aber mit mehreren Modulen arbeitest oder andere Module erstellst oder importierst, werden die beiden anderen Attribute wichtig:
declarations
-
Das
declarations
Attribut stellt sicher, dass Komponenten und Direktiven innerhalb des Geltungsbereichs des Moduls verwendet werden können. Das Angular CLI fügt deine Komponente oder Direktive automatisch zum Modul hinzu, wenn du eine Komponente über das Modul erstellst. Wenn du mit der Erstellung von Angular-Anwendungen beginnst, vergisst du leicht, deine neu erstellten Komponenten demdeclarations
Attribut hinzuzufügen. Behalte das im Auge (wenn du nicht das Angular CLI verwendest!), um diesen häufigen Fehler zu vermeiden. imports
-
Mit dem Attribut
imports
kannst du Module angeben, die du importieren und innerhalb deines Moduls zugänglich machen möchtest. Dies dient vor allem dazu, Module von Drittanbietern einzubinden, damit die Komponenten und Dienste in deiner Anwendung verfügbar sind. Wenn du eine Komponente aus anderen Modulen verwenden willst, musst du sicherstellen, dass du die entsprechenden Module in das Modul importierst, das du deklariert hast und in dem die Komponente existiert. exports
-
Das Attribut
exports
ist wichtig, wenn du entweder mehrere Module hast oder eine Bibliothek erstellen musst, die von anderen Entwicklern verwendet werden soll. Wenn du eine Komponente nicht exportierst, kann sie nicht außerhalb des direkten Moduls, in dem sie deklariert wurde, verwendet werden. Als allgemeine Faustregel gilt: Wenn du die Komponente in einem anderen Modul verwenden musst, solltest du sie exportieren.
Tipp
Wenn du Probleme bei der Verwendung einer Komponente hast, weil Angular eine Komponente nicht erkennt oder sagt, dass es ein Element nicht erkennt, liegt das höchstwahrscheinlich an falsch konfigurierten Modulen. Überprüfe der Reihe nach die folgenden Punkte:
-
Ob die Komponente als Deklaration im Modul hinzugefügt wird.
-
Wenn es sich nicht um eine Komponente handelt, die du selbst geschrieben hast, musst du sicherstellen, dass du das Modul importiert hast, das die Komponente bereitstellt/exportiert.
-
Wenn du eine neue Komponente erstellt hast, die in anderen Komponenten verwendet werden soll, stelle sicher, dass du die Komponente in ihrem Modul exportierst, damit jede Anwendung, die das Modul enthält, Zugriff auf deine neu erstellte Komponente erhält.
Eingabe und Ausgabe
Ein häufiger Anwendungsfall, wenn wir mit der Erstellung von Komponenten beginnen, ist, dass wir den Inhalt, den eine Komponente verwendet, von der Komponente selbst trennen wollen. Eine Komponente ist dann wirklich nützlich, wenn sie wiederverwendbar ist. Eine der Möglichkeiten, eine Komponente wiederverwendbar zu machen (anstatt fest kodierte Standardwerte in ihr zu haben), besteht darin, je nach Anwendungsfall unterschiedliche Eingaben zu machen. Ebenso kann es Fälle geben, in denen wir von einer Komponente Hooks verlangen, wenn eine bestimmte Aktivität in ihrem Kontext stattfindet.
Angular bietet Hooks, um jedes dieser Elemente durch Dekoratoren zu spezifizieren, die passenderweise Input
und Output
heißen. Im Gegensatz zu den Dekoratoren Component
und NgModule
gelten diese auf der Ebene der Klassenvariablen.
Eingabe
Wenn wir einen Input
Dekorator für eine Membervariable hinzufügen, kannst du über die Datenbindungssyntax von Angular automatisch Werte an übergeben, die die Komponente für diese bestimmte Eingabe enthält.
Schauen wir uns an, wie wir unsere stock-item
Komponente aus dem vorherigen Kapitel erweitern können, damit wir das Bestandsobjekt übergeben können, anstatt es in der Komponente selbst zu kodieren. Das fertige Beispiel findest du im GitHub-Repository im Ordner chapter4/component-input. Wenn du das Beispiel nachbauen möchtest und nicht über den vorherigen Code verfügst, kannst du die Codebasis aus Kapitel3/ng-if als Ausgangspunkt für den Code verwenden.
Zunächst ändern wir die Komponente stock-item
, um den Bestand als Eingabe für die Komponente zu markieren, aber anstatt das Bestandsobjekt zu initialisieren, markieren wir es als Input
für die Komponente. Dazu importieren wir den Dekorator und verwenden ihn für die Variable stock
. Der Code für die Datei stock-item.component.ts sollte wie folgt aussehen:
import
{
Component
,
OnInit
,
Input
}
from
'@angular/core'
;
import
{
Stock
}
from
'../../model/stock'
;
@
Component
({
selector
:
'app-stock-item'
,
templateUrl
:
'./stock-item.component.html'
,
styleUrls
:
[
'./stock-item.component.css'
]
})
export
class
StockItemComponent
{
@
Input
()
public
stock
:Stock
;
constructor
()
{
}
toggleFavorite
(
event
)
{
this
.
stock
.
favorite
=
!
this
.
stock
.
favorite
;
}
}
Wir haben die gesamte Instanziierungslogik aus der Komponente app-stock-item
entfernt und die Variable stock
als Eingabe markiert. Das bedeutet, dass die Initialisierungslogik ausgelagert wurde und die Komponente nur noch dafür verantwortlich ist, den Wert der Aktie von der übergeordneten Komponente zu erhalten und die Daten darzustellen.
Als Nächstes schauen wir uns die AppComponent
an und wie wir sie ändern können, um die Daten an die StockItemComponent
zu übergeben:
import
{
Component
,
OnInit
}
from
'@angular/core'
;
import
{
Stock
}
from
'app/model/stock'
;
@
Component
({
selector
:
'app-root'
,
templateUrl
:
'./app.component.html'
,
styleUrls
:
[
'./app.component.css'
]
})
export
class
AppComponent
implements
OnInit
{
title
=
'Stock Market App'
;
public
stockObj
:Stock
;
ngOnInit
()
:
void
{
this
.
stockObj
=
new
Stock
(
'Test Stock Company'
,
'TSC'
,
85
,
80
);
}
}
Wir haben soeben die Initialisierung des Bestandsobjekts von der StockItemComponent
in die AppComponent
verschoben. Schauen wir uns abschließend die Vorlage der AppComponent
an, um zu sehen, wie wir den Bestand an die StockItemComponent
übergeben können:
<h1>
{{title}}</h1>
<app-stock-item
[
stock
]="
stockObj
"
></app-stock-item>
Wir verwenden die Datenbindung von Angular, um den Bestand von AppComponent
an StockItemComponent
zu übergeben. Der Name des Attributs (stock
) muss mit dem Namen der Variable in der Komponente übereinstimmen, die als Eingabe markiert wurde. Beim Attributnamen wird zwischen Groß- und Kleinschreibung unterschieden, also achte darauf, dass er genau mit dem Namen der Eingabevariablen übereinstimmt. Der Wert, den wir an das Attribut übergeben, ist die Referenz des Objekts in der Klasse AppComponent
, die stockObj
ist.
HTML und Groß-/Kleinschreibung bei Attributen?
Du fragst dich vielleicht, wie das überhaupt möglich ist. Angular verfügt über einen eigenen HTML-Parser, der die Templates auf Angular-spezifische Syntax hin analysiert und sich für einige davon nicht auf die DOM-API verlässt. Aus diesem Grund wird bei Angular-Attributen zwischen Groß- und Kleinschreibung unterschieden.
Diese Eingänge sind datengebunden, d.h. wenn du den Wert des Objekts in AppComponent
änderst, wird dies automatisch in dem untergeordneten Objekt StockItemComponent
wiedergegeben.
Ausgabe
Genauso wie wir Daten an eine Komponente übergeben können, können wir uns auch registrieren und auf Ereignisse von einer Komponente hören. Wir verwenden die Datenbindung, um Daten zu übergeben, und wir verwenden die Syntax der Ereignisbindung, um uns für Ereignisse zu registrieren. Hierfür verwenden wir den Output
Dekorator.
Wir registrieren eine EventEmitter
als Ausgabe von einer beliebigen Komponente. Dann können wir das Ereignis mit dem EventEmitter
Objekt auslösen, so dass jede Komponente, die an das Ereignis gebunden ist, die Benachrichtigung erhält und entsprechend handeln kann.
Wir können den Code aus dem vorherigen Beispiel verwenden, in dem wir einen Input
Dekorator registriert haben, und von dort aus weitermachen. Erweitern wir nun die StockComponent
, um ein Ereignis auszulösen, wenn sie favorisiert wird, und verlagern wir die Datenmanipulation von der Komponente zu ihrer Mutterkomponente. Auch das ist sinnvoll, denn die übergeordnete Komponente ist für die Daten verantwortlich und sollte die einzige Quelle der Wahrheit sein. Wir lassen also die übergeordnete Komponente AppComponent
sich für das Ereignis toggleFavorite
registrieren und ändern den Zustand des Bestands, wenn das Ereignis ausgelöst wird.
Der fertige Code dafür befindet sich im Ordner chapter4/component-output.
Wirf einen Blick auf den StockItemComponent
Code in src/app/stock/stock-item/stock-item.component.ts:
import
{
Component
,
OnInit
,
Input
,
Output
,
EventEmitter
}
from
'@angular/core'
;
import
{
Stock
}
from
'../../model/stock'
;
@
Component
({
selector
:
'app-stock-item'
,
templateUrl
:
'./stock-item.component.html'
,
styleUrls
:
[
'./stock-item.component.css'
]
})
export
class
StockItemComponent
{
@
Input
()
public
stock
:Stock
;
@
Output
()
private
toggleFavorite
:EventEmitter
<
Stock
>
;
constructor
()
{
this
.
toggleFavorite
=
new
EventEmitter
<
Stock
>
();
}
onToggleFavorite
(
event
)
{
this
.
toggleFavorite
.
emit
(
this
.
stock
);
}
}
Ein paar wichtige Dinge sind zu beachten:
-
Wir haben den
Output
Dekorator sowie dieEventEmitter
aus der Angular-Bibliothek importiert. -
Wir haben ein neues Klassenmitglied namens
toggleFavorite
vom TypEventEmitter
erstellt und unsere Methode inonToggleFavorite
umbenannt. DieEventEmitter
kann für zusätzliche Typsicherheit typisiert werden. -
Wir müssen sicherstellen, dass die Instanz
EventEmitter
initialisiert wird, da sie nicht automatisch für uns initialisiert wird. Entweder machst du das inline oder im Konstruktor, wie wir es vorher gemacht haben. -
Die
onToggleFavorite
ruft jetzt einfach eine Methode auf derEventEmitter
auf, um das gesamte Bestandsobjekt zu senden. Das bedeutet, dass alle Zuhörer des EreignissestoggleFavorite
das aktuelle Bestandsobjekt als Parameter erhalten.
Wir werden auch stock-item.component.html ändern, um die Methode onToggleFavorite
statt toggleFavorite
aufzurufen. Das HTML-Markup bleibt ansonsten ziemlich gleich:
<div
class=
"stock-container"
>
<div
class=
"name"
>
{{stock.name + ' (' + stock.code + ')'}}</div>
<div
class=
"price"
[
class
]="
stock
.
isPositiveChange
()
?
'
positive
'
:
'
negative
'"
>
$ {{stock.price}}</div>
<button
(
click
)="
onToggleFavorite
($
event
)"
*
ngIf=
"!stock.favorite"
>
Add to Favorite</button>
</div>
Als Nächstes fügen wir dem AppComponent
eine Methode hinzu, die immer dann ausgelöst werden soll, wenn die Methode onToggleFavorite
ausgelöst wird, die wir mit einer Ereignisbindung versehen:
import
{
Component
,
OnInit
}
from
'@angular/core'
;
import
{
Stock
}
from
'app/model/stock'
;
@
Component
({
selector
:
'app-root'
,
templateUrl
:
'./app.component.html'
,
styleUrls
:
[
'./app.component.css'
]
})
export
class
AppComponent
implements
OnInit
{
title
=
'app works!'
;
public
stock
:Stock
;
ngOnInit
()
:
void
{
this
.
stock
=
new
Stock
(
'Test Stock Company'
,
'TSC'
,
85
,
80
);
}
onToggleFavorite
(
stock
:Stock
)
{
console
.
log
(
'Favorite for stock '
,
stock
,
' was triggered'
);
this
.
stock
.
favorite
=
!
this
.
stock
.
favorite
;
}
}
Die einzige Neuerung ist die Methode onToggleFavorite
, die einen Bestand als Argument benötigt. In diesem speziellen Fall verwenden wir den übergebenen Bestand nur für die Protokollierung, aber du kannst jede Entscheidung/Arbeit darauf stützen. Beachte auch, dass der Name der Funktion nicht relevant ist und du sie nennen kannst, wie du willst.
Zum Schluss fügen wir alles zusammen, indem wir die neue Ausgabe von unserer StockComponent
in der Datei app-component.html:
<h1>
{{title}}</h1>
<app-stock-item
[
stock
]="
stock
"
(
toggleFavorite
)="
onToggleFavorite
($
event
)"
>
</app-stock-item>
Wir haben soeben eine Ereignisbindung mit der Angular-Ereignisbindungssyntax zu der in der Komponente stock-item
deklarierten Ausgabe hinzugefügt. Beachte auch hier die Groß- und Kleinschreibung und die exakte Übereinstimmung mit der Membervariable, die wir mit dem Output
Dekorator dekoriert haben. Um Zugriff auf den von der Komponente ausgegebenen Wert zu erhalten, verwenden wir das Schlüsselwort $event
als Parameter für die Funktion. Ohne dieses Schlüsselwort würde die Funktion immer noch ausgelöst werden, aber du würdest keine Argumente erhalten.
Wenn du die Anwendung ausführst (erinnere dich, ng serve
), solltest du die voll funktionsfähige App sehen und wenn du auf die Schaltfläche Zu Favoriten hinzufügen klickst, sollte sie die Methode in der AppComponent
auslösen.
Erkennung von Veränderungen
Wir haben changeDetection
als Attribut für den Component
Dekorator erwähnt. Nachdem wir nun gesehen haben, wie die Dekoratoren Input
und Output
funktionieren, wollen wir ein wenig tiefer in die Erkennung von Änderungen auf Komponentenebene durch Angular eintauchen.
Standardmäßig wendet Angular den ChangeDetectionStrategy.Default
Mechanismus auf das changeDetection
Attribut an. Das bedeutet, dass Angular jedes Mal, wenn es ein Ereignis bemerkt (z. B. eine Serverantwort oder eine Benutzerinteraktion), jede Komponente im Komponentenbaum durchläuft und jede der Bindungen einzeln überprüft, um zu sehen, ob sich Werte geändert haben und in der Ansicht aktualisiert werden müssen.
Bei einer sehr großen Anwendung wirst du viele Bindungen auf einer bestimmten Seite haben. Wenn ein Nutzer eine Aktion ausführt, weißt du als Entwickler vielleicht genau, dass sich der größte Teil der Seite nicht ändern wird. In solchen Fällen kannst du dem Angular-Änderungsdetektor einen Hinweis geben, damit er bestimmte Komponenten prüft oder nicht prüft, wie du es für richtig hältst. Für jede beliebige Komponente können wir dies erreichen, indem wir ChangeDetectionStrategy
von der Standardeinstellung auf ChangeDetectionStrategy.OnPush
ändern. Damit wird Angular mitgeteilt, dass die Bindungen für diese bestimmte Komponente nur auf der Grundlage von Input
überprüft werden müssen.
Schauen wir uns ein paar Beispiele an, um zu sehen, wie das ablaufen könnte. Nehmen wir an, wir haben einen Komponentenbaum A → B → C. Das heißt, wir haben eine Wurzelkomponente A, die in ihrer Vorlage eine Komponente B verwendet, die wiederum eine Komponente C verwendet. Und nehmen wir an, dass Komponente B ein zusammengesetztes Objekt compositeObj
als Eingabe an Komponente C weitergibt. Vielleicht so etwas wie:
<c [inputToC]="compositeObj"></c>
Das heißt, inputToC
ist die Eingangsvariable, die mit dem Input
Dekorator in der Komponente C markiert ist und das Objekt compositeObj
von der Komponente B übergeben bekommt. Angenommen, wir markieren das Attribut changeDetection
der Komponente C als ChangeDetectionStrategy.OnPush
. Hier sind die Auswirkungen dieser Änderung:
-
Wenn die Komponente C Bindungen zu irgendwelchen Attributen von
compositeObj
hat, funktionieren diese wie gewohnt (keine Änderung gegenüber dem Standardverhalten). -
Wenn die Komponente C Änderungen an einem der Attribute von
compositeObj
vornimmt, werden diese ebenfalls sofort aktualisiert (keine Änderung gegenüber dem Standardverhalten). -
Wenn die übergeordnete Komponente B eine neue
compositeObj
erstellt oder die Referenz voncompositeObj
ändert (z. B. durch einen neuen Operator oder eine Zuweisung aus einer Serverantwort), dann würde Komponente C die Änderung erkennen und ihre Bindungen für den neuen Wert aktualisieren (keine Änderung des Standardverhaltens, aber das interne Verhalten ändert sich, je nachdem, wie Angular die Änderung erkennt). -
Wenn die übergeordnete Komponente B ein Attribut auf
compositeObj
direkt ändert (als Reaktion auf eine Benutzeraktion außerhalb der Komponente B), werden diese Änderungen in der Komponente C nicht aktualisiert (eine wichtige Änderung gegenüber dem Standardverhalten). -
Wenn die übergeordnete Komponente B ein beliebiges Attribut als Reaktion auf einen Event Emitter von Komponente C ändert und dann ein beliebiges Attribut auf
compositeObj
ändert (ohne die Referenz zu ändern), würde dies trotzdem funktionieren und die Bindungen würden aktualisiert werden. Das liegt daran, dass die Änderung von der Komponente C ausgeht (keine Änderung des Standardverhaltens).
Angular bietet Möglichkeiten, uns zu signalisieren, wann wir die Bindungen auch innerhalb der Komponente überprüfen müssen, um die absolute Kontrolle über die Datenbindung von Angular zu haben. Wir werden diese in "Change Detection" behandeln . Für den Moment ist es gut, den Unterschied zwischen den beiden Änderungserkennungsstrategien zu verstehen, die Angular bietet.
Ändern wir nun den Beispielcode, um dies in Aktion zu sehen. Ändere zunächst die Datei stock-item.component.ts, um die ChangeDetectionStrategy
in der untergeordneten Komponente zu ändern:
import
{
Component
,
OnInit
,
Input
,
Output
}
from
'@angular/core'
;
import
{
EventEmitter
,
ChangeDetectionStrategy
}
from
'@angular/core'
;
import
{
Stock
}
from
'../../model/stock'
;
@
Component
({
selector
:
'app-stock-item'
,
templateUrl
:
'./stock-item.component.html'
,
styleUrls
:
[
'./stock-item.component.css'
],
changeDetection
:ChangeDetectionStrategy.OnPush
})
export
class
StockItemComponent
{
@
Input
()
public
stock
:Stock
;
@
Output
()
private
toggleFavorite
:EventEmitter
<
Stock
>
;
constructor
()
{
this
.
toggleFavorite
=
new
EventEmitter
<
Stock
>
();
}
onToggleFavorite
(
event
)
{
this
.
toggleFavorite
.
emit
(
this
.
stock
);
}
changeStockPrice() {
this
.
stock
.
price
+=
5
;
}
}
Wir haben nicht nur ChangeDetectionStrategy
geändert, sondern auch eine weitere Funktion zu changeStockPrice()
hinzugefügt. Wir werden diese Funktionen nutzen, um das Verhalten der Änderungserkennung im Kontext unserer Anwendung zu demonstrieren.
Als Nächstes ändern wir die Datei stock-item.component.html, damit wir die neue Funktion auslösen können. Wir fügen einfach eine neue Schaltfläche hinzu, die den Aktienkurs auslöst und ändert, wenn die Schaltfläche angeklickt wird:
<div
class=
"stock-container"
>
<div
class=
"name"
>
{{stock.name + ' (' + stock.code + ')'}}</div>
<div
class=
"price"
[
class
]="
stock
.
isPositiveChange
()
?
'
positive
'
:
'
negative
'"
>
$ {{stock.price}}</div>
<button
(
click
)="
onToggleFavorite
($
event
)"
*
ngIf=
"!stock.favorite"
>
Add to Favorite</button>
<button
(
click
)="
changeStockPrice
()"
>
Change Price</button>
</div>
Das HTML der Vorlage wird nicht verändert, außer dass eine neue Schaltfläche zum Ändern des Aktienkurses hinzugefügt wird. Ändern wir also schnell die Hauptdatei app.component.html, um eine weitere Schaltfläche hinzuzufügen, mit der die Änderung des Kurses von der übergeordneten Komponente ausgelöst wird (ähnlich wie bei Komponente B im vorherigen hypothetischen Beispiel):
<h1>
{{title}}</h1>
<app-stock-item
[
stock
]="
stock
"
(
toggleFavorite
)="
onToggleFavorite
($
event
)"
>
</app-stock-item>
<button
(
click
)="
changeStockObject
()"
>
Change Stock</button>
<button
(
click
)="
changeStockPrice
()"
>
Change Price</button>
Wir haben zwei neue Schaltflächen zu dieser Vorlage hinzugefügt: eine, die die Referenz des Bestandsobjekts direkt ändert, und eine weitere, die die bestehende Referenz des Bestandsobjekts ändert, um den Preis von der übergeordneten AppComponent
zu ändern. Jetzt können wir endlich sehen, wie das alles in der Datei app.component.ts zusammenhängt:
import
{
Component
,
OnInit
}
from
'@angular/core'
;
import
{
Stock
}
from
'app/model/stock'
;
@
Component
({
selector
:
'app-root'
,
templateUrl
:
'./app.component.html'
,
styleUrls
:
[
'./app.component.css'
]
})
export
class
AppComponent
implements
OnInit
{
title
=
'app works!'
;
public
stock
:Stock
;
private
counter
:number
=
1
;
ngOnInit
()
:
void
{
this
.
stock
=
new
Stock
(
'Test Stock Company - '
+
this
.
counter
++
,
'TSC'
,
85
,
80
);
}
onToggleFavorite
(
stock
:Stock
)
{
// This will update the value in the stock item component
// Because it is triggered as a result of an event
// binding from the stock item component
this
.
stock
.
favorite
=
!
this
.
stock
.
favorite
;
}
changeStockObject() {
// This will update the value in the stock item component
// Because we are creating a new reference for the stock input
this
.
stock
=
new
Stock
(
'Test Stock Company - '
+
this
.
counter
++
,
'TSC'
,
85
,
80
);
}
changeStockPrice() {
// This will not update the value in the stock item component
// because it is changing the same reference and angular will
// not check for it in the OnPush change detection strategy.
this
.
stock
.
price
+=
10
;
}
}
Die Datei app.component.ts hat die meisten Änderungen erfahren. Der vorangehende Code ist ebenfalls mit Kommentaren versehen, die das erwartete Verhalten beim Auslösen der einzelnen Funktionen erklären. Wir haben zwei neue Methoden hinzugefügt: changeStockObject()
AppComponent
, die eine neue Instanz des Objekts stock
erstellt, und changeStockPrice()
, die die Preise des Objekts stock
in AppComponent
ändert. Wir haben auch einen Zähler hinzugefügt, um zu verfolgen, wie oft wir ein neues Bestandsobjekt erstellen, aber das ist nicht unbedingt notwendig.
Wenn du diese Anwendung jetzt ausführst, solltest du folgendes Verhalten erwarten:
-
Wenn du auf
StockItemComponent
auf "Zu Favoriten hinzufügen" klickst, funktioniert alles wie erwartet. -
Wenn du auf
StockItemComponent
auf "Preis ändern" klickst, wird der Preis der Aktie jedes Mal um $5 erhöht. -
Wenn du außerhalb von
StockItemComponent
auf "Bestand ändern" klickst, wird der Name des Bestands bei jedem Klick geändert. (Deshalb haben wir den Zähler hinzugefügt!) -
Wenn du außerhalb von
StockItemComponent
auf Change Price klickst, hat das keine Auswirkung (auch wenn der tatsächliche Wert der Aktie springt, wenn du danach auf Change Price klickst). Das zeigt, dass das Modell aktualisiert wird, aber Angular aktualisiert nicht die Ansicht.
Du solltest auch ChangeDetectionStrategy
auf die Standardeinstellungen zurücksetzen, um den Unterschied in der Praxis zu sehen.
Lebenszyklus einer Komponente
Komponenten (und Direktiven) in Angular haben ihren eigenen Lebenszyklus, von der Erstellung über das Rendern und Ändern bis hin zur Zerstörung. Dieser Lebenszyklus wird in der Reihenfolge der Baumnavigation von oben nach unten ausgeführt. Nachdem Angular eine Komponente gerendert hat, beginnt es den Lebenszyklus für jedes ihrer Kinder und so weiter, bis die gesamte Anwendung gerendert ist.
Es gibt Zeiten, in denen diese Lebenszyklus-Ereignisse für die Entwicklung unserer Anwendung nützlich sind. Deshalb bietet Angular Hooks für diesen Lebenszyklus, damit wir ihn beobachten und bei Bedarf reagieren können. Abbildung 4-1 zeigt die Lebenszyklus-Hooks einer Komponente in der Reihenfolge, in der sie aufgerufen werden.
Angular ruft zunächst den Konstruktor für jede Komponente auf und dann die verschiedenen, bereits erwähnten Schritte in der richtigen Reihenfolge. Einige davon, wie OnInit
und AfterContentInit
(im Grunde jeder Lifecycle-Hook, der auf Init
endet), werden nur einmal aufgerufen, wenn eine Komponente initialisiert wird, während die anderen immer dann aufgerufen werden, wenn sich ein Inhalt ändert. Der OnDestroy
Hook wird ebenfalls nur einmal für eine Komponente aufgerufen.
Für jeden dieser Lebenszyklusschritte gibt es eine Schnittstelle, die implementiert werden sollte, wenn sich eine Komponente um diesen bestimmten Lebenszyklus kümmert, und jede Schnittstelle bietet eine Funktion, die mit ng
beginnt und implementiert werden muss. Zum Beispiel muss für den Lebenszyklusschritt OnInit
eine Funktion namens ngOnInit
in der Komponente implementiert werden und so weiter.
Wir werden hier jeden einzelnen Lebenszyklusschritt durchgehen und dann anhand eines Beispiels die Reihenfolge der Lebenszyklusschritte innerhalb einer Komponente und zwischen den Komponenten veranschaulichen.
Es gibt noch ein weiteres Konzept zu lernen, das wir in diesem Kapitel kurz ansprechen und später ausführlicher behandeln werden - das Konzept von ViewChildren
und ContentChildren
.
ViewChildren
ist jede untergeordnete Komponente, deren Tags/Selektoren (meistens Elemente, da dies die Empfehlung für Komponenten ist) innerhalb der Vorlage der Komponente erscheinen. In unserem Fall wäre app-stock-item
also eine ViewChild
der AppComponent
.
ContentChildren
ist jede untergeordnete Komponente, die in die Ansicht der Komponente projiziert wird, aber nicht direkt in der Vorlage innerhalb der Komponente enthalten ist. Stell dir etwas wie ein Karussell vor, bei dem die Funktionalität in der Komponente gekapselt ist, aber die Ansicht, z. B. Bilder oder Buchseiten, vom Benutzer der Komponente stammt. Dies wird im Allgemeinen durch ContentChildren
erreicht. Wir werden das später in diesem Kapitel noch genauer behandeln.
Schnittstellen und Funktionen
Tabelle 4-1 zeigt die Schnittstellen und Funktionen in der Reihenfolge, in der sie aufgerufen werden, zusammen mit spezifischen Details zu dem Schritt, falls es etwas zu beachten gibt. Beachte, dass wir hier nur die komponentenspezifischen Lebenszyklusschritte behandeln, die sich leicht vom Lebenszyklus einer Richtlinie unterscheiden.
Versuchen wir, alle diese Hooks zu unserer bestehenden Anwendung hinzuzufügen, um die Reihenfolge der Ausführung in einem realen Szenario zu sehen. Wir fügen alle diese Hooks sowohl in unsere AppComponent
als auch in die StockItemComponent
ein, mit einer einfachen console.log
, um zu sehen, wann und wie diese Funktionen ausgeführt werden. Wir werden die Basis aus dem Output-Beispiel verwenden. Falls du also nicht mitcodierst, kannst du das Beispiel aus Kapitel4/component-output nehmen, um von dort aus zu bauen.
Das letzte fertige Beispiel findest du auch in Kapitel 4/Komponenten-Lebenszyklus.
Zuerst können wir die Datei src/app/app.component.ts ändern und die Hooks wie folgt hinzufügen:
import
{
Component
,
SimpleChanges
,
OnInit
,
OnChanges
,
OnDestroy
,
DoCheck
,
AfterViewChecked
,
AfterViewInit
,
AfterContentChecked
,
AfterContentInit
}
from
'@angular/core'
;
import
{
Stock
}
from
'app/model/stock'
;
@
Component
({
selector
:
'app-root'
,
templateUrl
:
'./app.component.html'
,
styleUrls
:
[
'./app.component.css'
]
})
export
class
AppComponent
implements
OnInit
,
OnChanges
,
OnDestroy
,
DoCheck
,
AfterContentChecked
,
AfterContentInit
,
AfterViewChecked
,
AfterViewInit
{
title
=
'app works!'
;
public
stock
:Stock
;
onToggleFavorite
(
stock
:Stock
)
{
console
.
log
(
'Favorite for stock '
,
stock
,
' was triggered'
);
this
.
stock
.
favorite
=
!
this
.
stock
.
favorite
;
}
ngOnInit
()
:
void
{
this
.
stock
=
new
Stock
(
'Test Stock Company'
,
'TSC'
,
85
,
80
);
console
.
log
(
'App Component - On Init'
);
}
ngAfterViewInit
()
:
void
{
console
.
log
(
'App Component - After View Init'
);
}
ngAfterViewChecked
()
:
void
{
console
.
log
(
'App Component - After View Checked'
);
}
ngAfterContentInit
()
:
void
{
console
.
log
(
'App Component - After Content Init'
);
}
ngAfterContentChecked
()
:
void
{
console
.
log
(
'App Component - After Content Checked'
);
}
ngDoCheck
()
:
void
{
console
.
log
(
'App Component - Do Check'
);
}
ngOnDestroy
()
:
void
{
console
.
log
(
'App Component - On Destroy'
);
}
ngOnChanges
(
changes
:SimpleChanges
)
:
void
{
console
.
log
(
'App Component - On Changes - '
,
changes
);
}
}
Wie du siehst, haben wir die Schnittstellen für OnInit, OnChanges, OnDestroy, DoCheck, AfterContentChecked, AfterContentInit, AfterViewChecked, AfterViewInit
in der Klasse AppComponent
implementiert und dann die entsprechenden Funktionen implementiert. Jede der Methoden gibt einfach ein Log-Statement aus, in dem der Komponentenname und der Name der Trigger-Methode angegeben werden.
Das Gleiche gilt für die StockItemComponent
:
import
{
Component
,
SimpleChanges
,
OnInit
,
OnChanges
,
OnDestroy
,
DoCheck
,
AfterViewChecked
,
AfterViewInit
,
AfterContentChecked
,
AfterContentInit
,
Input
,
Output
,
EventEmitter
}
from
'@angular/core'
;
import
{
Stock
}
from
'../../model/stock'
;
@
Component
({
selector
:
'app-stock-item'
,
templateUrl
:
'./stock-item.component.html'
,
styleUrls
:
[
'./stock-item.component.css'
]
})
export
class
StockItemComponent
implements
OnInit
,
OnChanges
,
OnDestroy
,
DoCheck
,
AfterContentChecked
,
AfterContentInit
,
AfterViewChecked
,
AfterViewInit
{
@
Input
()
public
stock
:Stock
;
@
Output
()
private
toggleFavorite
:EventEmitter
<
Stock
>
;
constructor
()
{
this
.
toggleFavorite
=
new
EventEmitter
<
Stock
>
();
}
onToggleFavorite
(
event
)
{
this
.
toggleFavorite
.
emit
(
this
.
stock
);
}
ngOnInit
()
:
void
{
console
.
log
(
'Stock Item Component - On Init'
);
}
ngAfterViewInit
()
:
void
{
console
.
log
(
'Stock Item Component - After View Init'
);
}
ngAfterViewChecked
()
:
void
{
console
.
log
(
'Stock Item Component - After View Checked'
);
}
ngAfterContentInit
()
:
void
{
console
.
log
(
'Stock Item Component - After Content Init'
);
}
ngAfterContentChecked
()
:
void
{
console
.
log
(
'Stock Item Component - After Content Checked'
);
}
ngDoCheck
()
:
void
{
console
.
log
(
'Stock Item Component - Do Check'
);
}
ngOnDestroy
()
:
void
{
console
.
log
(
'Stock Item Component - On Destroy'
);
}
ngOnChanges
(
changes
:SimpleChanges
)
:
void
{
console
.
log
(
'Stock Item Component - On Changes - '
,
changes
);
}
}
Wir haben genau das Gleiche gemacht wie auf AppComponent
mit StockItemComponent
. Jetzt können wir diese Anwendung ausführen, um sie in Aktion zu sehen.
Wenn du es ausführst, öffne die JavaScript-Konsole im Browser. Du solltest in der Reihenfolge der Ausführung sehen:
-
Zuerst wird die
AppComponent
erstellt. Dann werden die folgenden Hooks auf demAppComponent
ausgelöst:-
On Init
-
Do Check
-
After Content Init
-
After Content Checked
Die beiden vorangegangenen werden sofort ausgeführt, weil wir bisher keine Inhaltsprojektion in unserer Anwendung haben.
-
-
Als Nächstes wird die
StockItemComponent OnChanges
ausgeführt, wobei die Eingabe in dieStockItemComponent
als Änderung erkannt wird, gefolgt von den hier aufgeführten Haken innerhalb derStockItemComponent
:-
On Init
-
Do Check
-
After Content Init
-
After Content Checked
-
After View Init
-
After View Checked
-
-
Schließlich gibt es keine Unterkomponenten mehr, über die man nach unten traversieren könnte, also geht Angular zurück zum übergeordneten
AppComponent
und führt Folgendes aus:-
After View Init
-
After View Checked
-
Das gibt uns einen guten Überblick darüber, wie und in welcher Reihenfolge Angular bei der Initialisierung vorgeht und welche Baumstruktur es im Verborgenen durchläuft. Diese Hooks sind sehr nützlich für bestimmte knifflige Initialisierungslogiken und definitiv wichtig für das Aufräumen, wenn deine Komponente fertig ist, um Speicherlecks zu vermeiden.
Ansicht Projektion
Der letzte Punkt, den wir in diesem Kapitel behandeln werden, ist das Konzept der View-Projektion. Projektion ist eine wichtige Idee in Angular, da sie uns mehr Flexibilität bei der Entwicklung unserer Komponenten gibt und uns ein weiteres Werkzeug an die Hand gibt, um sie in verschiedenen Kontexten wiederverwendbar zu machen.
Die Projektion ist nützlich, wenn wir Komponenten bauen wollen, aber einige Teile der Benutzeroberfläche der Komponente nicht von Haus aus mit ihr verbunden sind. Nehmen wir zum Beispiel an, wir wollen eine Komponente für ein Karussell bauen. Ein Karussell hat ein paar einfache Fähigkeiten: Es kann ein Element anzeigen und uns ermöglichen, zum nächsten/vorherigen Element zu navigieren. Deine Karussellkomponente kann auch andere Funktionen haben, wie z. B. "Lazy Loading" usw. Eine Sache, die nicht in den Zuständigkeitsbereich der Karussellkomponente fällt, ist der Inhalt, den sie anzeigt. Ein Benutzer der Komponente könnte ein Bild, eine Buchseite oder irgendetwas anderes anzeigen lassen wollen.
In diesen Fällen wird die Ansicht also vom Benutzer der Komponente gesteuert, und die Funktionalität wird von der Komponente selbst bereitgestellt. Dies ist nur ein Anwendungsfall, in dem wir die Projektion in unseren Komponenten nutzen wollen.
Schauen wir uns an, wie wir die Inhaltsprojektion in unserer Angular-Anwendung nutzen können. Wir werden die Basis aus dem Input-Beispiel verwenden. Falls du also nicht mitcodierst, kannst du das Beispiel aus Kapitel 4/component-input nehmen und von dort aus aufbauen.
Das fertige Beispiel findest du in Kapitel 4/Komponentenprojektion.
Als erstes werden wir unsere StockItemComponent
ändern, um die Projektion von Inhalten zu ermöglichen. In unserer Komponentenklasse gibt es keine Codeänderung; wir müssen nur die Datei src/app/stock/stock-item/stock-item.component.html wie folgt ändern:
<div
class=
"stock-container"
>
<div
class=
"name"
>
{{stock.name + ' (' + stock.code + ')'}}
</div>
<div
class=
"price"
[
class
]
=
"
stock
.
isPositiveChange
(
)
?
'
positive
'
:
'
negative
'
"
>
$ {{stock.price}}
</div>
<ng-content
>
</ng-content>
</div>
Wir haben einfach die Schaltflächen entfernt, die wir vorher hatten, und lassen den Benutzer der Komponente entscheiden, welche Schaltflächen angezeigt werden sollen. Um dies zu ermöglichen, haben wir die Schaltflächen durch ein ng-content
Element ersetzt. Es sind keine weiteren Änderungen an der Komponente erforderlich.
Als Nächstes nehmen wir eine Änderung an AppComponent
vor, um einfach eine Methode für Testzwecke hinzuzufügen. Ändere die Datei src/app/app.component.ts wie folgt:
/** Imports and decorators skipped for brevity **/
export
class
AppComponent
implements
OnInit
{
/** Constructor and OnInit skipped for brevity **/
testMethod() {
console
.
log
(
'Test method in AppComponent triggered'
);
}
}
Wir haben einfach eine Methode hinzugefügt, die auf der Konsole protokolliert, wenn sie ausgelöst wird. Damit können wir nun sehen, wie wir unsere aktualisierte StockItemComponent
verwenden und die Macht der Projektion nutzen können. Ändere die Datei app.component.html wie folgt:
<h1>
{{title}}</h1>
<app-stock-item
[
stock
]="
stockObj
"
>
<button
(
click
)="
testMethod
()"
>
With Button 1</button>
</app-stock-item>
<app-stock-item
[
stock
]="
stockObj
"
>
No buttons for you!!</app-stock-item>
Wir haben zwei Instanzen der Komponente app-stock-item
in unserem HTML-Code hinzugefügt. Beide haben jetzt einen Inhalt, im Gegensatz zu früher, wo diese Elemente keinen Inhalt hatten. In dem einen Element befindet sich eine Schaltfläche, die die testMethod
auslöst, die wir in AppComponent
hinzugefügt haben, und das andere Element hat einfach nur Textinhalt.
Wenn wir unsere Angular-Anwendung ausführen und sie im Browser öffnen, sollten wir etwas wie in Abbildung 4-2 sehen.
Beachte, dass die beiden Bestandskomponenten in unserem Browser, die jeweils einen leicht unterschiedlichen Inhalt haben, auf den von uns bereitgestellten Daten basieren. Wenn du auf die Schaltfläche im ersten Aktien-Widget klickst, wirst du sehen, dass die Methode im AppComponent
aufgerufen und die console.log
ausgelöst wird.
So haben die Nutzer der Komponente jetzt die Möglichkeit, einen Teil der Benutzeroberfläche der Komponente nach eigenem Ermessen zu verändern. Wir können sogar auf Funktionen der übergeordneten Komponente zugreifen, was sie wirklich flexibel macht. Es ist auch möglich, mehrere verschiedene Abschnitte und Inhalte in unsere untergeordnete Komponente zu projizieren. Die offizielle Angular-Dokumentation ist zwar spärlich zu diesem Thema, aber es gibt einen tollen Artikel, der dir mehr Einblick in die Inhaltsprojektion gibt.
Fazit
In diesem Kapitel haben wir uns eingehend mit Komponenten befasst und einige der am häufigsten verwendeten Attribute bei der Erstellung von Komponenten kennengelernt. Wir haben einen detaillierten Blick auf den Component
Dekorator geworfen, über Attribute wie template
und templateUrl
gesprochen, über Stile und wie die Änderungserkennung von Angular funktioniert und wie wir sie außer Kraft setzen können.
Dann haben wir uns mit dem Lebenszyklus einer Komponente befasst und mit den Hooks, die Angular uns bietet, um auf einige dieser Lebenszyklus-Ereignisse zu reagieren. Zum Schluss haben wir uns mit der Projektion in Komponenten beschäftigt und damit, wie wir wirklich mächtige Komponenten erstellen können, die es dem Benutzer der Komponente ermöglichen, Teile der Benutzeroberfläche zu bestimmen.
Im nächsten Kapitel werden wir einen kurzen Abstecher machen, um das Unit Testing von Komponenten zu verstehen und zu sehen, wie wir sowohl die Logik, die die Komponente steuert, als auch die Ansicht, die gerendert wird, testen können.
Übung
Für unsere dritte Übung können wir auf der vorherigen Übung(Kapitel3/Übung) aufbauen, indem wir Konzepte aus diesem Kapitel einbeziehen:
-
Erstelle eine
ProductListComponent
. Initialisiere dort ein Array von Produkten, anstatt ein einzelnes Produkt in derProductComponent
zu initialisieren. Ändere die Vorlage so, dass sieNgFor
verwendet, um für jedes Produkt eineProductItemComponent
zu erstellen. -
Verwende Inline-Templates und Styles auf
ProductListComponent
. Erstelle sie mit dem Angular CLI mit dieser Einstellung, anstatt sie zu generieren und manuell zu ändern. -
Ändere die
ProductItemComponent
, um das Produkt als Eingabe zu verwenden. -
Verschiebe die Erhöhungs-/Verringerungslogik von
ProductItem
aufProductListComponent
. Verwende einen Index oder eine Produkt-ID, um das Produkt zu finden und seine Menge zu ändern. -
Verschiebe die
ProductItemComponent
so, dass sie optimal ist und wechsle vom StandardChangeDetectionStrategy
zu einemOnPush
ChangeDetectionStrategy
.
All dies kann mit den in diesem Kapitel behandelten Konzepten erreicht werden. Du kannst dir die fertige Lösung in Kapitel 4/Übung/Ökonomie ansehen.
Get Angular: Auf und davon now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.