Angular-Renaissance Teil 3: Die erweiterte Template-Syntax
Angular 17 hat die Template-Syntax erweitert und ebnet damit den Weg fĂĽr die zukĂĽnftige Signalkomponente. Der Artikel zeigt, wie man die Syntax verwendet.

(Bild: iX)
- Rainer Hahnekamp
Der dritte und letzte Teil in der Angular-Renaissance-Artikelserie widmet sich der erweiterten Template-Syntax, die seit Angular 17.0 als Developer Preview vorliegt. Dieser Neuzugang mag im Vergleich zur Hydration und Signalen unspektakulär erscheinen.
Ein Teilbereich davon sind die Deferrable Views – eine Innovation, mit der Angular eine Pionierrolle einnimmt. Code Splitting teilt die Anwendung in mehrere JavaScript-Dateien auf, wodurch nicht alles sofort geladen wird. Diese Technik ist nichts Neues, doch die Möglichkeit durch Deferrable Views, Code Splitting direkt im HTML und sogar nach verschiedenen Kriterien zu definieren, sucht ihresgleichen. In Angular 18 haben die Deferrable Views in dieser Woche den Preview-Modus verlassen.
Erweiterte Template-Syntax
Angular hat sich immer sehr stark an den HTML-Standard angelehnt und ist nur in Ausnahmefällen davon abgewichen. Mit Angular 17 ist es mit dieser Konformität allerdings vorbei.
Template Blocks, wie die erweiterte Syntax auch genannt wird, ersetzen die Structural Directives *ngIf
, *ngFor
und *ngSwitch
durch @if
, @for
sowie @switch
. Das vereinfacht zum einen die Syntax, andererseits wĂĽrden Structural Directives bei den kommenden Signalkomponenten (siehe Teil 2) aufgrund interner technischer GrĂĽnde auch nicht mehr funktionieren.
Mit den Anweisungen @if
, @for
und @switch
, die sich mit dem HTML vermischen, ist der Kontrollfluss direkt eingebettet. Kontrollstrukturen liegen also jetzt auĂźerhalb der Tags.
So sieht beispielsweise ein Template vor Angular 17 aus:
<ul>
<ng-container *ngFor="let country of countries">
<li *ngIf="country.locale === filteredLocale">{{ country.name }}</li>
</ng-container>
</ul>
Mit der neuen Template-Syntax ergibt sich nun folgender Code:
<ul>
@for (country of countries; track country.name) {
@if (country.locale === filteredLocale) {
<li>{{ country.name }}</li>
}
}
</ul>
Das Template einer Komponente besteht somit aus einer Mischung von Programmcode und HTML. Ă„hnlichkeiten zur JavaScript-Syntaxerweiterung JSX, wie man sie von React her kennt, sind durchaus gewollt.
Die Wahl des @
-Zeichens ist vom Framework Svelte inspiriert. Hier hat sich die Community durchgesetzt, denn das Angular-Team hatte ursprĂĽnglich eine andere Syntax im Sinn.
Bei dieser Änderung ist ein wesentlicher Punkt die Frage nach dem Wieso. Es mag sein, dass die neue Syntax ein wenig benutzerfreundlicher wirkt, muss man aber deswegen nun wieder seinen kompletten Code durchforsten und ändern? Die Gründe liegen ganz woanders: Die Syntaxänderung ist primär eine Weichenstellung für die zukünftige Signalkomponente.
Das Angular-Team hatte bereits weit vor Version 16 – und selbst bevor es die Signale ankündigte – Prototypen und Pläne für die Signalkomponente in der Schublade. Dabei dürfte sich offenbar herausgestellt haben, dass die Implementierung der aktuellen Structural Directives eine Nachbesserung benötigte, um in den Signalkomponenten laufen zu können.
Ferner war das Angular-Team mit der technischen Umsetzung dieser Directives nicht sehr glücklich. Zum Beispiel lässt die Developer Experience bei einem *ngIf
zu wĂĽnschen ĂĽbrig. Ein einfaches else
ist syntaktisch schon eine kleine Herausforderung, und ein elseif
gab es sowieso nie.
Dazu kommt, dass die Kontrollflusskommandos immer explizit importiert sein müssen. Nativ unterstützt das Template diese Funktionalität nicht. Bei modernen Frameworks gehört dies aber mittlerweile zum Standard. Eine native Unterstützung hätte auch Vorteile hinsichtlich der Performance, da der Angular-Compiler sich darum kümmern würde.
Es gibt also gewichtige Gründe für die erweiterte Syntax und dafür, dass Angular-Developer diese auch so bald wie möglich einsetzen.
Beispielsweise sind bei dem neuen @If
mehrere Verzweigungen kein Problem:
<ul>
@for (country of countries;track country.name) {
@if (country.name === 'Austria') {
<li>Ă–sterreich</li>
} @else if (country.locale === filteredLocale) {
<li>{{ country.name }}</li>
} @else {
<li class="unfiltered">{{ country.name }}</li>
}
}
</ul>
Möchte man nun so schnell wie möglich zur Tat schreiten, muss man bei bestehendem Code die einzelnen Templates durchforsten. Hier hilft es, dass Google mehrere Tausend Angular-Anwendungen intern betreibt und das Angular-Team auch den hausinternen Support leistet. Eine der internen Regeln besagt, dass dieser bei Breaking Changes das entsprechende Update durchführen muss. Das ist natürlich bei einer derartigen Menge manuell unmöglich, wodurch bei Angular standardmäßig automatische Skripts mitlaufen, die den Quellcode der Anwendungen umschreiben.
Die Structural Directives sind nicht als veraltet (deprecated) markiert und ein Umstieg somit nicht zwingend. Dennoch gibt es ein Skript, das die Migration automatisch durchfĂĽhrt. DafĂĽr reicht die AusfĂĽhrung des Befehls
ng g @angular/core:control-flow
Die Extended-Template-Syntax befindet sich noch in der Developer Preview. Das Angular-Team sieht jedoch grundsätzlich keine Instabilitäten oder Risiken. Als einer der Hauptgründe für den Status als Developer Preview gilt ein neuer Algorithmus in @for
, der das Rendering beschleunigt.
Deferred Loading / Deferrable Views
Deferred Loading ist das "Star Feature" in Angular 17.0. Es macht von den neuen Möglichkeiten des Templates Gebrauch und erlaubt, deklarativ und direkt im Template Komponenten als lazy loading zu definieren.
Zudem bietet es Kriterien an, wann diese Komponente einerseits geladen und andererseits aktiviert werden soll.
Der vielseitige Katalog an Kriterien schlieĂźt ein, dass beispielsweise Angular mit dem Laden beginnt, wenn der Platzhalter oder ein DOM-Element im Viewport, also sichtbar, auftaucht. Andere Kriterien sind ein Klick, ein Maus-Hover, der Ablauf einer voreingestellten Zeitspanne oder eine Funktion, bei der die Logik individuell programmierbar ist.
Die Komponente und ihre Abhängigkeiten in @defer
mĂĽssen standalone sein. Container fĂĽr @defer
kann jedoch eine NgModule-deklarierte Komponente sein.
Es folgt ein einfaches Beispiel zum Einstimmen.
Darin wird ĂĽber eine Liste von Holidays
iteriert, wobei jedes einzelne Element über eine UI-Komponente gerendert wird. Die Liste rendert jedoch nur dann, wenn die Benutzer zuvor die Allgemeinen Geschäftsbedingungen (AGB) per Klick auf einen Button bestätigt haben.
<h2>Choose among our Holidays</h2>
<button>I hereby agree to the terms</button>
<div class="flex flex-wrap justify-evenly">
@for (holiday of holidays(); track holiday.id) {
<app-holiday-card
[holiday]="holiday"
(addFavourite)="addFavourite($event)"
(removeFavourite)="removeFavourite($event)" />
}
</div>
Dies erreicht man durch ein HinzufĂĽgen des neuen @defer
-Blocks. In der Konsole zeigt sich dann sogleich, dass der Compiler eine neue JavaScript-Datei erstellt hat, die den Inhalt von @defer
samt Abhängigkeiten enthält.
Beim Aufruf der Seite zeigt Angular somit nur den Button an. Durch die Hinzugabe der Bedingung (on interaction(button))
löst erst der Klick auf den Button den Ladevorgang aus.
Wie der Screenshot zeigt, entfallen sofort 15 KByte. Bei einer Gesamtgröße von 35 KByte für das main
-Bundle ist das fĂĽr den Aufwand ein deutlicher Gewinn.
<h2>Choose among our Holidays</h2>
<button mat-raised-button #button>I hereby agree to the terms</button>
<div class="flex flex-wrap justify-evenly">
@defer (on interaction(button)) {
@for (holiday of holidays(); track byId($index, holiday)) {
<app-holiday-card
[holiday]="holiday"
(addFavourite)="addFavourite($event)"
(removeFavourite)="removeFavourite($event)" />
}
}
</div>
(Bild:Â Rainer Hahnekamp)
Wie oben angefĂĽhrt, gibt es mehrere Kriterien. Durch den Austausch von on interaction
durch etwa on viewport
lassen sich die verschiedenen Arten sehr einfach ausprobieren. Das Kombinieren unterschiedlicher Kriterien ist ebenfalls möglich.
In der Praxis wird man jedoch @defer
mit der Erweiterung @placeholder
verwenden. Anstatt also den Benutzern eine leere Fläche zu zeigen, kann man eine alternative Komponente oder einen Text darstellen.
Neben dem Placeholder gibt es die Möglichkeit, für den Ladeprozess einen separaten Block zu definieren. Ladeprozess meint die Zeitspanne, bis der Browser die Bundle-Datei geladen hat.
Sollte sich die geladene Komponente von sich aus mit dem Backend verbinden und Daten abfragen, fällt das nicht unter den Ladeprozess, da die Komponente bereits aktiv ist.
Daher wird eher @placeholder
eine Rolle spielen und Benutzer mit langsamer Bandbreite werden den Block innerhalb von @loading
zu Gesicht bekommen:
<h2>Choose among our Holidays</h2>
@defer (on interaction; on timer(10s)) {
<div class="flex flex-wrap justify-evenly">
@for (holiday of holidays(); track byId($index, holiday)) {
<app-holiday-card
[holiday]="holiday"
(addFavourite)="addFavourite($event)"
(removeFavourite)="removeFavourite($event)"
/>
}
</div>
} @placeholder {
<button mat-raised-button>Show me the holidays.</button>
} @loading {
<p>Loading Holidays...</p>
}
Ein wenig hat sich der Anwendungsfall nun verändert. Der Button ist in den @placeholder
gewandert, da dieser Bereich ohnehin immer angezeigt wird. Standardmäßig gilt bei Vorhandensein des Platzhalters auch dieser als Referenz für die Kriterien viewport
, interaction
etc. Deswegen ist auch die Template-Variable (#button
) nicht mehr notwendig.
Ferner gibt es ein zweites Kriterium. Der Ladevorgang benötigt zwar weiterhin den Klick, sollte dies jedoch nicht innerhalb von zehn Sekunden erfolgen, lädt Angular die Holidays automatisch nach.
Um den Effekt von @loading
zu sehen, lohnt es sich, ĂĽber die Browsertools die Netzwerkgeschwindigkeit zu drosseln. Ansonsten vernimmt man nur einen kurzen "Flickereffekt" zwischen der Platzhalter- und der eigentlichen Anzeige. Mit langsamem Netzwerk sieht man fĂĽr die Dauer des Ladens den Text "Loading Holidays".
Es ist jedoch noch mehr möglich: @loading
bietet eine Mindestdauer fĂĽr die Ladezeit an. Erst wenn der Browser diese ĂĽberschreitet, aktiviert sich der @loading
-Block. Um den Flickereffekt zu verhindern, kann das Loading – falls es überhaupt angezeigt wird – für eine definierbare Zeitspanne sichtbar bleiben, auch wenn in der Zwischenzeit Angular die Komponente vollständig geladen hat.
Eine weitere Möglichkeit ist die prefetch
-Option. Damit wĂĽrde Angular bereits vorzeitig mit dem Laden des Bundles starten, aber auf die Aktivierung warten.
Im folgenden Beispiel sieht man sowohl den Einsatz des Prefetching als auch des parametrisierten @loading
:
<h2>Choose among our Holidays</h2>
@defer (on interaction; on timer(10s); prefetch on viewport) {
<div class="flex flex-wrap justify-evenly">
@for (holiday of holidays(); track byId($index, holiday)) {
<app-holiday-card
[holiday]="holiday"
(addFavourite)="addFavourite($event)"
(removeFavourite)="removeFavourite($event)"
/>
}
</div>
} @placeholder {
<button mat-raised-button>Show me the holidays.</button>
} @loading (after 100ms; minimum 1s) {
<p>Loading Holidays...</p>
}
Angular beginnt hier mit vorzeitigem Laden, sobald der Platzhalter in den Viewport kommt. Die Komponente aktiviert sich jedoch erst nach zehn Sekunden oder wenn die Benutzer auf den Button klicken. Sollte bis dahin das Laden noch nicht im Gange sein, wird es spätestens dann gestartet.
Besteht eine langsame Internetverbindung und das Herunterladen der 15 KByte dauert länger als 100 Millisekunden, erscheint für den Benutzer minimal eine Sekunde lang die Ladeanzeige. Sollte der Ladevorgang länger dauern, bleibt die Ladeanzeige natürlich erhalten.
Trotz der vielen Regeln in diesem Beispiel bleibt der finale Code sehr gut leserlich.
@deferred
ist nur der Anfang. Die Möglichkeit, ehemals komplizierte Features per deklarativem Code sehr einfach zu gestalten, zeigt das Potenzial auf.
So verkündete etwa Jeremy Elbourn, Tech Lead des kompletten Angular-Projekts (Framework, CLI, Material), dass es durchaus möglich wäre, Serverkomponenten, wie man sie von React kennt, zu integrieren.
Anstatt eines @defer
wĂĽrde man einen Block beispielsweise als @server
markieren. Dieser wĂĽrde dann ausschlieĂźlich am Server rendern und bis auf das statische HTML wĂĽrde der Serverprozess kein JavaScript an den Browser schicken.
Zusammenfassung und Ausblick
Die aktuell in sozialen Medien zu findende Behauptung, dass "ein wenig Bewegung in Angular gekommen ist", ist untertrieben, denn Angular erfindet sich derzeit neu und schafft es dennoch, abwärtskompatibel zu sein. Wie in dieser Artikelserie erläutert, brachten die Versionen von Angular 17 neue und nützliche Features, sind aber auch Vorbereitungen für kommende Releases in diesem Jahr – darunter das am 23. Mai 2024 vorgestellte Angular 18.
Hydration und Signale sind in ihren Grundfunktionen bereits vorhanden und stabil. Die Hydration soll laut dem Entwicklungsteam zur technisch fortgeschrittensten Progressive Hydration werden. Signale übernehmen immer mehr Funktionen, wie man etwa mit den Signal Inputs bereits sehen kann. Das Meisterstück werden schlussendlich die Signalkomponenten sein, die performancetechnisch auf höchstem Niveau laufen werden.
(mai)