
Alles unter Kontrolle - Control Flow für Angular
Angular unterstützt mit dem neuen Release sogenannte Control Flow Blocks. Die deklarative Syntax stellt die Integration der Funktionen der bekannten Directiven (ngIf, ngFor und ngSwitch) direkt in das Framework dar. Diese Artikel beschäftig sich mit der Syntax und zeigt diese an Beispielen auf. Auch wenn ich alte und neue Syntax vergleich, setze ich entsprechendes Wissen der alten Syntax voraus und was man sie einsetzt und wann nicht. Ich werde nicht auf defer eingehen, dazu sicherlich ein weiterer Artikel zu einer späteren Zeit.
Die neue Syntax und damit die Integration in das Frame hat durchaus verschiedenen Vorteile. Die neue Syntax ist deutlich einfacher zu lesen. Die Darstellung in eigenen Zeilen (man muss dies nicht tun, ich empfehle es aber dringend), gibt der Deklaration einfach mehr Bedeutung und lässt sich so auch besser warten. Darüber hinaus, werden nun auch keine Tags mehr benötigt. Besonders wenn man nur Text ausgeben wollte, musste man einen ng-container verwenden. Auch ein Performanzgewinn ist spürbar. Angular selbst spricht von einem hohen zweistelligen Prozentgewinn, aber das ist natürlich immer Abhängig wie viel die Bedingungen in den jeweiligen Projekten zum Einsatz kommen. Ich kann nur sagen, dass sämtliche Änderungen in den letzten Releasen einen sehr spürbaren Effekt hatten.
In der Community wurde heiß diskutiert, wie die neue Syntax auszusehen hat und vor allem wir das Framework die Steuerung erkennt und sicherstellt, dass nichts falsch interpretiert wird. Die Diskussionen waren sehr lang (kann man sich gern auf github ansehen) und die Ideen vielfältig, man hat sich letztendlich für das @ entschieden und zwar vor allen Wörtern und nicht nur am Anfang der Sektionen. Ein Blick auf die Beispiele zeigt, was damit gemeint ist und so kommen wir auch direkt zum ersten Abschnitt.
Die Beispiele sind alle aus einem Projekt, welches sich damit beschäftig, ein Frontend eines Blogs abzubilden. Dennoch sind diese stark vereinfach, ich habe unwichtige Properties weggelassen.
@if
Kommen wir zunächst zu einem einfachen Beispiel, wir wollen ein Bild anzeigen, wenn eines konfiguriert ist.
<img *ngIf="article.imageUrl" [ngSrc]="article.imageUrl" ...>
Die neue Variante
@if (article.imageUrl) {
<img [ngSrc]="article.imageUrl" ...>
}
Klar die neue Variant ist länge aber auch aufgeräumter. Es ist klar wo genau die Bedingung ist. Beim *ngIf obliegt es den Entwicklern zu entschieden, ob sie es als erstes auf dem Tag haben wollen.
@if und Asynchronität
Natürlich wird das noch unterstützt:
<div *ngIf="article$ | async as article" class="article">
<h1>{{ article.headline }}</h1>
</div>
Und das sieht jetzt wie folgt aus
@if (article$ | async; as article) {
<div class="article">
<h1>{{ article.headline }}</h1>
</div>
}
Hier wird meiner Meinung nach deutlicher, dass es besser sein kann, die Deklaration eine eigene Zeile zu gönnen. Damit kann man es sehr einfach erfassen. Bitte beachtet den kleinen Unterschied, es ist nun ein ";" nötig, um danach das Ergebnis in eine Variable zu packen.
Bei bei der Nutzung von Signals ist das natürlich analog.
@if (article$(); as article) {
<div class="article">
<h1>{{ article.headline }}</h1>
</div>
}
In meinem Beispiel schreibe ich Signals ebenfalls mit "$"
@if und @else
Jetzt will man ja nicht immer nur etwas ein- bzw. ausblenden, sondern will auch eine alternative anbieten.
<img *ngIf="article.imageUrl; #noImage" [ngSrc]="article.imageUrl" ...>
<ng-template #noImage>
<img ngSrc="/assets/fallback-img.png" ...>
</ng-template>
Man verwies auf ein Template, welches in der selben Datei definiert war.
@if (article.imageUrl) {
<img [ngSrc]="article.imageUrl" ...>
} @else {
<img ngSrc="/assets/fallback-img.png" ...>
}
Sieht das nicht viel besser und aufgeräumter aus? Hier wird auch nochmal klar, dass "@" immer davor geschrieben werden muss.
@if und @else if
Und nun kommt eine deutliche Verbesserung, es ist eben nicht immer alles binär:
@if (article.imageUrl) {
<img [ngSrc]="article.imageUrl" ...>
} @else if (article.altText) {
<p>{{ article.altText }}</p>
} @else {
<img ngSrc="/assets/fallback-img.png" ...>
}
Das war in der alten Schreibweise deutlich umständlicher zu schreiben.
@switch
Das Beispiel oben sollte man natürlich nicht übertreiben. Besonders wenn in den Statements immer die selbe Variable mit etwas verglichen wird.
<div [ngSwitch]="content.contentType">
<ng-container [*ngSwitchCase]="ContentType.Article">
<app-article [article]="content"></app-article>
</ng-container>
<ng-container [*ngSwitchCase]="ContentType.Comment">
<app-comment [comment]="content"></app-comment>
</ng-container>
<ng-container *ngSwitchDefault>
<app-simple-text [content]="content"></app-simple-text>
</ng-container>
</div>
Diese Variante empfand ich immer ab schlimmsten. Besonders wenn man nur kurz darauf schaut, erfasst man nicht die Struktur der Logik.
@switch (content.contentType) {
@case (ContentType.Article) {
<app-article [article]="content"></app-article>
}
@case (ContentType.Comment) {
<app-comment [comment]="content"></app-comment>
}
@default {
<app-simple-text [content]="content"></app-simple-text>
}
}
Ich hoffe bei diesem Beispiel wird der Vorteil der besseren Leserlichkeit besonders klar.
@for
Kommen wir nun zu dem Fall, dass wir viele Daten darstellen wollen. Das könnten eine Liste von Teasern sein.
<ul>
<li *ngFor="let teaser of filteredTeasers$ | async; trackBy: trackByFn; let last = last">
<a [href]="teaser.permalink" [routerLink]="teaser.permalink">
<h2>{{ teaser.title }}</h2>
</a>
<div [innerHtml]="teaser.metaData?.ingress?.value | sanitizeHtml"></div>
<app-tag-list [tags]="teaser.tags"></app-tag-list>
<hr *ngIf="!last">
</li>
</ul>
Und nun in neu:
<ul>
@for (teaser of filteredTeasers$ | async; track teaser.id; let last = $last) {
<li>
<a [href]="teaser.permalink" [routerLink]="teaser.permalink">
<h2>{{ teaser.title }}</h2>
</a>
<div [innerHtml]="teaser.metaData?.ingress?.value | sanitizeHtml"></div>
<app-tag-list [tags]="teaser.tags"></app-tag-list>
<hr *ngIf="!last">
</li>
}
</ul>
Auch hier sehe ich wieder den Vorteil, dass man die eigentliche Schleife schneller sieht. Aber der eigentliche Vorteil ist "track". Grundsätzlich gab es das mit "trackBy" auch vorher schon, aber nun ist es verpflichtend und es ist deutlich einfacher zu nutzen. Die Funktion kann man nun direkt im Template angeben. Vorher hat man eine Funktion im Typescript Code definiert oder ein Pipe bemüht, die das gemacht hat.
Folgende Variablen stehen zur Verfügung (in meinem Beispiel habe ich $last verwendet): $count, $index, $first; $last, $even, $odd. Bei der Definition schreibt man zu erst "let" dann folgen die Zuweisungen per "," voreinander getrennt.
Zudem gibt es nun ein kleinen Bonus:
@for (...) {
...
} @empty {
<p>Keine Artikel gefunden</p>
}
Man benötigt keine zusätzliche Überprüfung ob eine Liste leer ist, das wird direkt mit übernommen.
Allgemeine Hinweise und Migration Der "alte" Weg, dies zu implementieren, bleibt natürlich erhalten. Dennoch ist es ratsam, sich auf einen Weg festzulegen. Dies sollte natürlich der neue sein. Ich habe es oben bereits angesprochen, dass hierbei die Performanz der entscheidende Faktor sein sollte.
Man kann Schrittweise migrieren, auch in einem Template lassen sich beide Varianten gleichzeitig nutzen.
Die Migration kann man automatisch durchführen lassen: ng g @angular/core:control-flow Dabei kann man den Pfad angeben und die Anzahl der Änderungen klein halten. Das würde ich vor allem bei großen Teams und viel Code empfehlen. Dabei ist manuelle Arbeit einzuplanen, besonders die Migration von "trackBy" zu "track" hat nicht immer perfekt geklappt. Durch die neue Syntax ist eine Korrektur aber sehr einfach.
Viel Spaß beim Ausprobieren
Referenzen:
https://angular.dev/guide/templates/control-flow