Neuigkeiten von trion.
Immer gut informiert.

Integration von Angular in Astro

Astro

Im letzten Blogpost über Astro wurde das Framework kurz vorgestellt und dessen Vorteile aufgezeigt. Einer dieser Vorteile hebt sich von den anderen besonders ab. Mit Astro ist man nicht auf nur ein Frontend-Framework beschränkt, sondern kann aus einer vielzahl der beliebtesten Frameworks auswählen. React, Svelte, Vue oder Solid ? Kein Problem!

Doch wie sieht es mit Angular aus? Kann es auch in Astro integriert werden? Und wenn ja, wie gut kann man mit Angular in Astro arbeiten?

Angular

Angular ist ein von Google entwickeltes Web-Framework, das von vielen genutzt und geliebt wird. Bei der Stackoverflow-Umfrage 2024 lag Angular bei den Professionellen Nutzern auf dem 4. Platz.

Genauso wie React ist Angular ein komponentenbasiertes Framework, das es ermöglicht, gekapselte Komponenten zu schreiben und wiederzuverwenden. Für das Zustandsmanagement verwendet Angular sogenannte Signals. Angular beobachtet diese Signals und analysiert, wie der Zustand in der gesamten Anwendung genutzt wird. Darauf basierend wird das Rendering der Anwendung optimiert.

Im Gegensatz zu React verfügt Astro standardmäßig Support unter anderem für Routing und Formulare.

In diesem Blogbeitrag wird eine kleine Beispielanwendung entwickelt, die sowohl Astro- als auch Angular-Komponenten verwendet, um ein Gefühl dafür zu bekommen, wie gut die Angular-Integration in Astro ist.

Beispiel Projekt Setup

Um mit Astro loslegen zu können, muss zuerst ein neues Projekt erstellt werden.

npm create astro@latest
  • Wähle einen Projektnamen aus (im Blog Beispiel "trion_example"),

  • wähle das Minimal Setup aus,

  • und installiere die Dependencies.

Die Projektstruktur sieht ähnlich wie bei anderen Frameworks aus.

├── public/
├── src/
├── astro.config.mjs
├── package.json
└── tsconfig.json

Im src Ordner werden Komponenten, Pages und Styles definiert. In public werden assets wie Icons und Fonts abgelegt.

Desweiteren gibt es die klassische package.json, eine tsconfgi.json und die für Astro wichtige astro.config.mjs.

Nun muss noch die Angular Integration installiert werden.

npx astro add @analogjs/astro-angular

Außerdem muss für die Angular Integration im Stammverzeichnis der Anwendung eine tsconfig.app.json eingefügt werden mit folgendem Inhalt.

{
  "extends": "./tsconfig.json",
  "compileOnSave": false,
  "compilerOptions": {
    "baseUrl": "./",
    "outDir": "./dist/out-tsc",
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "noImplicitOverride": true,
    "noPropertyAccessFromIndexSignature": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "sourceMap": true,
    "declaration": false,
    "downlevelIteration": true,
    "experimentalDecorators": true,
    "moduleResolution": "node",
    "importHelpers": true,
    "noEmit": false,
    "target": "es2020",
    "module": "es2020",
    "lib": ["es2020", "dom"],
    "skipLibCheck": true
  },
  "angularCompilerOptions": {
    "enableI18nLegacyMessageIdFormat": false,
    "strictInjectionParameters": true,
    "strictInputAccessModifiers": true,
    "strictTemplates": true,
    "allowJs": false
  },
  "files": [],
  "include": ["src/**/*.ts", "src/**/*.tsx"]
}

Damit ist das Setup abgeschlossen und es kann mit Angular begonnen werden.

Die erste Angular Komponente

Für die Beispielanwendung soll eine Angular Komponente für den Header erstellt werden. Diese Komponente wird dann in dem andererseits statischen Inhalt der Seite als Astro Island eingebunden.

Dafür wird zuerst die Komponenten Datei erstell.

└── src/
    └── components/
        └── header.compontent.ts

Der Aufbau einer Angular Komponent sieht wie folgt aus.

// src/components/header.component.ts

import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';

@Component({
  selector: 'app-header',
  standalone: true,
  imports: [CommonModule],
  template: `
  <header>
  </header>
  `,
})

export class HeaderComponent {
}

Es wird die Angular Component importiert um eine Komponente zu erstellen. CommonModule importiert alle gängigen Funktionen die am häufigsten verwendet werden.

Eine Komponente wird mit einem selector definiert der ein eindeutiger Key ist. Die Komponente ist als standalone markiert, dass bedeutet, dass alle Dependencies in der Komponente selbst definiert werden müssen. Die Dependencies werden in das imports feld eingefügt. Das Template ist HTML welche die Komponente beschreibt. Das Template kann auch in eine externe Datei ausgelagert werden. Dafür muss dann der Key templateUrl angegeben werden mit dem Pfad zur HTML Datei. Die Klasse HeaderComponent definiert den JavaScript Code für die Komponente.

Für den Header wird einfach ein Logo dargestellt und ein Login Button. Sobald der User sich eingeloggt hat, wird dessen Name neben dem Login Button angezeigt. Das HTML Template dafür sieht wie folgt aus.

<header class="flex flex-col p-2 space-y-1">
  <nav class="flex flex-row grow items-center justify-between">
    <img src="trion.svg" class="h-12"/>
    <div class="flex flex-row justify-end items-center space-x-2">
      <div *ngIf="username">Logged in as: {{ username }}</div>
      <div class="flex bg-trion-light rounded-lg items-center justify-center px-2 py-1 active:bg-trion-dark">
        <button (click)="login()">{{ isLoggedIn ? 'Logout' : 'Login' }}</button>
      </div>
    </div>
  </nav>
  <div class="flex grow h-1 bg-gray-300 rounded-lg"></div>
</header>

Mit *ngIf kann geprüft werden, ob der ein Benutzername gesetzt ist. Mit doppelten Klammern {{}} kann JavaScript verwendet werden.

In der Klasse wird zunächst eine Variable für den Benutzernamen angelegt und ein Boolean für den Login Status.

username: string | null = null;
isLoggedIn = false;

Also nächstes wird die Klasse erweitert mit OnInit.

export class HeaderComponent implements OnInit

Das ermöglicht es uns Code auszuführen sobald die Komponente iniziert wird. Es wird geprüft ob die LocalStorage API verfügbar ist und setzen dann die Werte.

ngOnInit() {
  if (typeof window !== 'undefined') {
    this.username = localStorage.getItem('username');
    this.isLoggedIn = !!this.username;
  }
}

Zuletzt muss noch die login() Funktion definiert werden.

login() {
  if (this.isLoggedIn) {
    localStorage.removeItem('username');
    this.username = null;
    this.isLoggedIn = false;
    window.location.href = '/';
  } else {
    window.location.href = '/login';
  }
}

Diese Funktion prüft ob der Benutzer schon eingeloggt ist. Ist er das, wird der LocalStorage bereinigt und die Werte zurückgesetzt und zuletzt wieder auf die Homepage geschaltet. Wenn der Benutzer nicht angemeldet ist, wird er auf das Login Form verwiesen.

Die ganze vollständige Komponente sieht dann wie folgt aus.

export class HeaderComponent implements OnInit {
  username: string | null = null;
  isLoggedIn = false;

  ngOnInit() {
    if (typeof window !== 'undefined') {
      this.username = localStorage.getItem('username');
      this.isLoggedIn = !!this.username;
    }
  }

  login() {
    if (this.isLoggedIn) {
      localStorage.removeItem('username');
      this.username = null;
      this.isLoggedIn = false;
      window.location.href = '/';
    } else {
      window.location.href = '/login';
    }
  }
}

Um nun die Komponente in unsere Index Page einzufügen muss sie lediglich importiert werden.

// src/pages/index.astro

import { HeaderComponent } from "../components/header.component";

Danach verwenden wir sie in der HTML Struktur.

<html lang="en">
	<head>
		<meta charset="utf-8" />
		<meta name="viewport" content="width=device-width" />
		<meta name="generator" content={Astro.generator} />
		<title>Trion</title>
	</head>
  <body>
    <HeaderComponent client:load />
    <main class="flex flex-row px-16 h-[calc(100vh-12rem)]">
      <div class="flex flex-row grow items-center justify-between">
        <div class="flex flex-col items-center space-y-4">
          <h2>Willkommen bei Trion!</h2>
          <p>Wir bieten großartige Dienstleistungen an.</p>
        </div>
        <div class="flex flex-col size-80 items-center text-center space-y-4">
          <img src="web-development.png"/>
          <p class="font-mono">"Wir verbinden Vision mit Code und schaffen Lösungen, die morgen schon Standard sind."</p>
        </div>
      </div>
    </main>
  </body>
</html>

Das client:load ist hier besonders wichtig. Damit sagen wir Astro, dass unsere Komponente als Island genutzt werden soll und beim laden der Seite mit geladen werden soll. Wenn client:load nicht angegeben wird, wird nur der reine HTML Code geladen aber nicht der JavaScript Code. Astro wirft standardmäßig jeglichen JavaScript Code von Board.

Damit wurde eine Angular Komponente erfolgreich in Astro eingebunden.

Fazit

Vor diesem Blogbeitrag hatte ich keinerlei Berührungspunkte mit Angular. Dieser Beitrag ist entstanden, um nach einem neuen Tech-Stack zu suchen für statische Websiten. Um nicht auf ein neues Frontend-Framework umzusteigen, sollte untersucht werden, wie gut sich Angular in Astro integrieren lässt.

Anfangs kann es schwerer sein, sich in Angular zurechtzufinden, wenn man beispielsweise von React kommt. Wenn man jedoch einmal verstanden hat, wie Angular strukturiert ist, kann man sehr intuitiv damit arbeiten.

Die Kombination von Angular und Astro kann sehr leistungsstark sein. So lassen sich statische Seiten mit hoher Performance bereitstellen. Dennoch können dynamische Elemente mit einem etablierten Framework integriert werden.

Allerdings wird die Integration von analogjs/astro-angular noch nicht von sehr vielen genutzt. Es könnten noch einige Fehler auftreten und derzeit werden nur Standalone-Komponenten unterstützt.

Die Integration wird jedoch von analogjs, einem Fullstack-Angular-Framework, entwickelt und gewartet. Dies macht die Nutzung der Integration vielversprechend. Allerdings muss jeder die Risiken für sich selbst abschätzen.

Feedback oder Fragen zu einem Artikel - per Twitter @triondevelop oder E-Mail freuen wir uns auf eine Kontaktaufnahme!

Los geht's!

Bitte teilen Sie uns mit, wie wir Sie am besten erreichen können.