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.

Lesezeit: 12 Min.
In Pocket speichern
vorlesen Druckansicht Kommentare lesen 6 Beiträge

(Bild: Andrey Suslov/Shutterstock.com)

Von
  • Rainer Hahnekamp
Inhaltsverzeichnis

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-Konferenz zu Enterprise-JavaScript

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:

Weitere Informationen zur enterJS sowie Zugang zu den Tickets bietet die Konferenzwebseite.

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.

Angular vor Standalone Components

Angular mit Standalone Components

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.

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 ihrer imports Property.
  • Stellt ein NgModule die entsprechende Komponente bereit, dann muss dieses NgModule die HelloComponent in ihrem imports 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.

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",
      },
    ],
  },
];