Angular 14 entschlackt und bringt mehr Typsicherheit
Die neue Angular-Hauptversion bringt Standalone Components und Typed Forms. Das verringert die Notwendigkeit für NgModules und bringt mehr Typsicherheit.
- Rainer Hahnekamp
Das bekannte JavaScript-Framework Angular aus dem Hause Google ist in Version 14 erschienen. Im Vorfeld gab es bereits große Vorfreude darauf, weil sich vor allem durch die Einführung der sogenannten Standalone Components die Komplexität des Frameworks signifikant reduziert. Zudem stechen noch die Typed Forms hervor, die Typsicherheit in die Behandlung von Formularen einbringen.
Daneben gibt es noch diverse weitere Features, wie beispielsweise, dass sich über die Routerkonfiguration direkt der Titel bestimmen lässt, der schlussendlich von Angular dynamisch im <title>
Tag gesetzt wird. Als eine weitere Neuerung gibt es eine inject()
-Funktion, die eine Alternative für die Dependency Injection über den Constructor darstellt. Sie lässt sich etwa statt des typenlosen @Inject()
verwenden. Auch hier steht Typsicherheit im Vordergrund.
Angular Material sowie das Angular CLI sind wie immer im Release inkludiert und auch diese bekamen neue Features spendiert. Dieser Artikel betrachtet jedoch die beiden Hauptänderungen Standalone Components und Typed Forms im Detail. Der verwendete Code samt vollständiger Anwendung ist auf GitHub abrufbar.
heise Developer und dpunkt.verlag richten am 22. und 23. Juni 2022 die enterJS im Darmstadtium in Darmstadt aus. Die JavaScript-Konferenz bietet zahlreiche Vorträge und Workshops zu JavaScript im Allgemeinen, die Frameworks (Angular, Node.js, React und Svelte) im Speziellen sowie TypeScript, Tools und Techniken rund um die Programmiersprache.
Neben mehr als 35 Vorträgen finden sich die folgenden Workshops im Programm:
- Performance at scale – High Speed Enterprise Angular
- Ein vollständiges JavaScript-Anwendungssystem
- Einführung in Playwright
- Moderne Web-Backends in Node.js
Weitere Informationen zur enterJS sowie Zugang zu den Tickets bietet die Konferenzwebseite.
Standalone Components und APIs
Das Angular-Framework untergliedert sich grob in fünf Hauptelemente. Als "visuelle Elemente" sind hier die Component, die Directive sowie die Pipe zu nennen. Diese Gruppe hat gemeinsam, dass sie über HTML-Code direkt angesprochen wird und dann auch ihrerseits weiteren HTML-Code generiert. Eigentlich genauso, wie man es auch von anderen Single-Page-Application-Frameworks (SPA) kennt.
Daneben gibt es Services, die normale Klassen sind und für die klassische Programmlogik herangezogen werden. Sie sind in der Dependency Injection vorhanden und können von den oben genannten "visuellen Elementen", aber natürlich auch von anderen Services injiziert werden.
Für das Bereitstellen von Services, also das Providing, gibt es zwei Möglichkeiten: Die Services stellen sich selbst bereit oder aber ein NgModule übernimmt das. Es gäbe noch eine dritte Möglichkeit über die Komponente, die allerdings sehr selten vorkommt. Die eigentliche Aufgabe eines NgModule ist jedoch das Bereitstellen der Komponenten (Pipes und Directives sind ab sofort mitgemeint) und auch das Bereitstellen von deren Abhängigkeiten.
An dieser Stelle fällt schon auf, dass mehr Komplexität besteht, als notwendig wäre. Beispielsweise war das Vorhandensein des NgModule immer eine Notwendigkeit, die vom Framework stammt und für Entwicklerinnen und Entwickler keinen wirklichen Mehrwert hätte.
Ab Version 14 ist es nun möglich, gar keine NgModules mehr zu verwenden. Das heißt allerdings nicht, dass Angular die Abhängigkeiten für die Komponenten automatisch bereitstellt. Vielmehr können das die Komponenten nun selbst tun. Dadurch kommt auch der Name "Standalone Components" zustande.
Was war der Grund, dass diese Neuerung erst jetzt erfolgte? Angular hat seine interne Rendering Engine umgeschrieben und sie mit Version 9 eingeführt. Mit der neuen Engine war es bereits intern so, dass die Komponenten selbst ihre Abhängigkeiten verwalteten beziehungsweise darüber Bescheid wussten. Entwickler mussten diese aber nach wie vor über NgModules bereitstellen.
Seit dem "Fiasko" des Wechsels von Angular Version 1 zu 2, der sehr viele Entwickler durch Inkompatibilität vergrämte, achtet das Angular-Team sehr penibel darauf, wenige oder bestenfalls gar keine Breaking Changes mehr einzuführen.
Das ist natürlich alles andere als einfach und kostet Zeit. Überspitzt lässt sich sagen, es hat von Angular 9 bis Angular 14, also mehr als 2 Jahre, gedauert. Erst jetzt kann man aus diesen internen Änderungen Profit schlagen und neue Features – unter Beibehaltung der Abwärtskompatibilität – anbieten.
Einsatz von Standalone Components
Legt man eine neue Komponente mittels dem Befehl ng generate component hello
an, dann ist von den Neuerungen nichts zu sehen. Der Grund liegt darin, dass Standalone Components als Developer Preview erschienen sind. Das bedeutet allerdings nicht, dass deren Einsatz nun hochriskant wäre. Das Angular-Team räumt sich lediglich das Recht ein, Breaking Changes einführen zu können.
Um Standalone Components zu verwenden, lässt sich die standalone
Property im @Component
-Dekorator direkt angeben. Alternativ lässt sich das Flag standalone
bei ng generate
hinzufügen. Das wäre dann ng generate component hello –-standalone
.
Man erhält folgenden Code:
Component({
selector: "app-hello",
standalone: true,
imports: [CommonModule],
templateUrl: "./hello.component.html",
styleUrls: ["./hello.component.scss"],
})
export class HelloComponent implements OnInit {
constructor() {}
ngOnInit(): void {}
}
Ein NgModule ist jetzt nicht mehr nötig. Hat diese Komponente nun beispielsweise Abhängigkeiten zu Angular Material und davon ein Button und ein Icon verwendet, so mussten Entwickler früher die entsprechenden Module beim NgModule importieren. Das ist Geschichte. Neben standalone
gibt es nun auch eine neue imports
Property, die genau dieselbe Funktion hat wie imports
beim NgModule.
Das heißt, die erstellte Komponente würde nun mit weiteren Abhängigkeiten folgendermaßen aussehen:
@Component({
selector: "app-hello",
standalone: true,
imports: [CommonModule, MatIconModule, MatButtonModule],
templateUrl: "./hello.component.html",
styleUrls: ["./hello.component.scss"],
})
export class HelloComponent implements OnInit {
constructor() {}
ngOnInit(): void {}
}
Sollte diese Komponente von einer anderen Komponente aufgerufen oder aber über das Routing definiert werden, dann muss diese natürlich auch dort importiert werden.
Und genau da ist die Abwärtskompatibilität im Einsatz zu sehen:
- Ist die entsprechende Component auch eine Standalone, dann importiert sie die
HelloComponent
in ihrerimports
Property. - Stellt ein NgModule die entsprechende Komponente bereit, dann muss dieses NgModule die
HelloComponent
in ihremimports
wie ein einfaches NgModule hinzufügen.
Das klingt sehr einfach – und das ist es auch. Es wird bei Migrationsprojekten zwar sehr viel manuelle Arbeit notwendig sein, die allerdings größtenteils aus Tipparbeit bestehen und keine Raketenwissenschaft erfordern wird. Das Angular-Team hat hierfür Automatisierungstools in Aussicht gestellt. Es gibt mittlerweile aber auch schon einige Community-Projekte, die Abhilfe versprechen.
Ein Blick auf Standalone APIs
Es wurde bisher gezeigt, wie sich Komponenten ohne NgModules schreiben lassen, aber damit sind diese noch nicht ganz weg. In Angular gibt es Spezialmodule, die Aufgaben erfüllen, die sich nicht ohne weiteres mit Standalone Components lösen lassen.
Dazu zählt zum Beispiel das AppModule
, das von Angular selbst zum Hochfahren des Frameworks und der gesamten Anwendung gebraucht wird. Es gibt Module, die Services bereitstellen. Da wäre vor allem das HttpClientModule
zu nennen, das den HttpClient
zur Backend-Kommunikation zur Verfügung stellt. Schlussendlich gibt es noch konfigurierbare NgModule wie das RouterModule
. Es bietet mit forRoot
und forChild
statische Methoden an, um das Routingsystem aufzusetzen.
Das Angular-Team stellt mit Version 14 neue Funktionen bereit, um auch diese Modultypen teilweise zu ersetzen. Das AppModule
lässt sich mit der Funktion bootstrapApplication
austauschen. Unter der Voraussetzung, dass bereits die AppComponent
eine Standalone Component ist, müssen nur noch die standardmäßigen NgModules wie HttpClientModule
beziehungsweise die Routenkonfiguration inkludiert werden.
Diese NgModule, die Services bereitstellen, sind unter der provides
Property anzugeben. Sie fungieren demnach wie normale Services, werden allerdings mit der neuen Methode importProvidersFrom
gewrappt.
Für die Routenkonfiguration bietet es sich an, kein explizites AppRouting Module
mehr zu verwenden. Stattdessen lässt sich die Routenkonfiguration als einfache Variable in einer Datei hinterlegen und dann über die bootstrapApplication
-Methode das RouterModule
in den provides
direkt mit der Routenkonfiguration aufrufen.
// ersetzt das komplette app.module.ts
bootstrapApplication(AppComponent, {
providers: [
importProvidersFrom(
BrowserAnimationsModule,
StoreModule.forRoot({}),
EffectsModule.forRoot([]),
RouterModule.forRoot(routes),
HttpClientModule
),
],
});
Um bei den Routen zu bleiben, stellt sich die Frage, wie nun der Umgang mit Lazy Loading ist. Hier war es bis dato so, dass das Routingsystem ein NgModule benötigte. Auch das wurde angepasst. Es ist nun möglich, direkt die Routes
statt des NgModule anzugeben.
Darüber hinaus haben auch die Routes
die Möglichkeit, Services über den Befehl importProvidersFrom
zur Verfügung zu stellen. Das erlaubt es nun, "konfigurierbare Module" wie zum Beispiel NgRx Feature States, die erst über das Lazy Loading aktiviert werden, in den neuen Modus zu überführen.
Das Beispiel zeigt ein Lazy-Loaded-Modul vor und nach Standalone Components.
Vor Standalone Components & APIs:
// app.routes.ts
export const routes: Routes = [
{
path: "",
component: HomeComponent,
},
{ path: "sign-up", component: SignUpComponent },
{
path: "holidays",
loadChildren: () =>
import("./holidays/holidays.module.ts").then((m) => m.HolidaysModule),
},
];
// holidays.module.ts
@NgModule({
declarations: [HolidaysComponent, HolidayCardComponent],
imports: [
CommonModule,
RouterModule.forChild([
{
path: "",
children: [
{
path: "",
component: HolidaysComponent,
},
],
},
]),
StoreModule.forFeature(holidaysFeature),
EffectsModule.forFeature([HolidaysEffects]),
// weitere NgModule...
],
})
export class HolidaysModule {}
Mit Standalone Components und APIs:
// app.routes.ts
export const routes: Routes = [
{
path: "",
component: HomeComponent,
title: "Eternal",
},
{ path: "sign-up", component: SignUpComponent, title: "Sign up" },
{
path: "holidays",
loadChildren: () =>
import("./holidays/holidays.routes").then((m) => m.holidayRoutes),
},
];
// holidays.routes.ts
export const holidayRoutes: Routes = [
{
path: "",
canActivate: [HolidaysDataGuard],
providers: [
importProvidersFrom([
StoreModule.forFeature(holidaysFeature),
EffectsModule.forFeature([HolidaysEffects]),
]),
],
children: [
{
path: "",
component: HolidaysComponent,
title: "Holidays",
},
],
},
];