Angular Standalone Components für moderne, modulfreie Apps — Claude Code baut Signals, Standalone Routing, Deferred Views und HttpClient ohne NgModule.
Mit Angular 17 und 18 hat Angular den größten Architekturwandel seit Jahren vollzogen: NgModule ist optional. Standalone Components sind jetzt die empfohlene Standardarchitektur — schlanker, testbarer und besser für Tree-Shaking geeignet. Claude Code versteht diese moderne Angular-Welt und generiert sauberen, typisierten, produktionsreifen Code ohne den NgModule-Overhead.
Der klassische Angular-Einstiegspunkt war AppModule mit platformBrowserDynamic().bootstrapModule(). In der Standalone-Welt ersetzt bootstrapApplication() diesen Pattern vollständig. Das Ergebnis ist eine schlanke main.ts mit einer expliziten ApplicationConfig.
// main.ts — kein NgModule, kein AppModule
import { bootstrapApplication } from '@angular/platform-browser' ;
import { AppComponent } from './app/app.component' ;
import { appConfig } from './app/app.config' ;
bootstrapApplication(AppComponent , appConfig)
.catch ((err) => console.error (err));
// app.config.ts
import { ApplicationConfig } from '@angular/core' ;
import { provideRouter, withComponentInputBinding } from '@angular/router' ;
import { provideHttpClient, withFetch, withInterceptors } from '@angular/common/http' ;
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async' ;
import { routes } from './app.routes' ;
import { authInterceptor } from './interceptors/auth.interceptor' ;
export const appConfig : ApplicationConfig = {
providers: [
provideRouter (routes, withComponentInputBinding ()),
provideHttpClient (
withFetch (),
withInterceptors ([authInterceptor])
),
provideAnimationsAsync (),
],
};
// app.component.ts
import { Component } from '@angular/core' ;
import { RouterOutlet, RouterLink } from '@angular/router' ;
@Component ({
selector: 'app-root' ,
standalone: true , // standalone: true = kein NgModule nötig
imports: [RouterOutlet, RouterLink], // direkte Imports statt NgModule
template: `
<nav>
<a routerLink="/">Home</a>
<a routerLink="/dashboard">Dashboard</a>
</nav>
<router-outlet />
` ,
})
export class AppComponent {}
Angular Signals sind die neue reaktive Primitive: synchron, granular und ohne Zone.js-Abhängigkeit. Sie ersetzen nicht RxJS, sondern ergänzen es — toSignal() und toObservable() bilden die Brücke zwischen beiden Welten.
// counter.component.ts
import { Component, signal, computed, effect } from '@angular/core' ;
@Component ({
selector: 'app-counter' ,
standalone: true ,
template: `
<div class="counter">
<p>Count: {{ count() }}</p>
<p>Double: {{ doubled() }}</p>
<p>Status: {{ status() }}</p>
<button (click)="increment()">+1</button>
<button (click)="decrement()">-1</button>
<button (click)="reset()">Reset</button>
</div>
` ,
})
export class CounterComponent {
// Writable Signal — mit signal(initialValue) erstellen
count = signal (0 );
// Computed Signal — automatisch bei Abhängigkeits-Änderung neu berechnet
doubled = computed (() => this .count () * 2 );
status = computed (() =>
this .count () > 0 ? 'positiv' : this .count () < 0 ? 'negativ' : 'null'
);
constructor() {
// Effect — Seiteneffekt bei Signal-Änderung (z.B. Logging, Analytics)
effect (() => {
console.log (`Counter geändert: ${this .count ()}` );
});
}
increment () { this .count.update ((v) => v + 1 ); }
decrement () { this .count.update ((v) => v - 1 ); }
reset () { this .count.set (0 ); }
}
import { Component, input, output, model } from '@angular/core' ;
@Component ({
selector: 'app-user-card' ,
standalone: true ,
template: `
<div>
<h3>{{ name() }}</h3>
<p>{{ role() ?? 'Kein Rolle' }}</p>
<input [(ngModel)]="username" /> <!-- two-way mit model() -->
<button (click)="onSelect()">Auswählen</button>
</div>
` ,
})
export class UserCardComponent {
// Signal Input — required oder optional mit Default
name = input .required <string >();
role = input <string | undefined >(undefined );
// Two-Way-Binding via model()
username = model ('' );
// Output als Signal-basiertes EventEmitter
selected = output <string >();
onSelect () {
this .selected.emit (this .name ());
}
}
import { Component, signal, computed } from '@angular/core' ;
import { toSignal, toObservable } from '@angular/core/rxjs-interop' ;
import { HttpClient } from '@angular/common/http' ;
import { switchMap } from 'rxjs/operators' ;
@Component ({ standalone: true , ... })
export class SearchComponent {
searchTerm = signal ('' );
// Signal → Observable → Signal Pipeline
results = toSignal (
toObservable (this .searchTerm).pipe (
debounceTime (300 ),
switchMap ((term) =>
this .http.get <Result []>(`/api/search?q= ${term}` )
),
),
{ initialValue: [] as Result [] }
);
constructor(private http: HttpClient ) {}
}
| Signal API | Beschreibung | Use Case |
|---|---|---|
signal(value) | Schreibbares Signal erstellen | Lokaler State |
computed(() => ...) | Abgeleiteter, gecachter Wert | Berechneter State |
effect(() => ...) | Seiteneffekt bei Änderung | Logging, DOM-Sync |
input() | Signal-basierter @Input() | Component-Props |
output() | Signal-basierter @Output() | Event-Emission |
model() | Two-Way-Signal-Binding | Formulare |
toSignal(obs$) | Observable → Signal | RxJS-Integration |
toObservable(sig) | Signal → Observable | RxJS-Pipelines |
Standalone Routing eliminiert Routing-Module vollständig. Statt RouterModule.forRoot() in AppModule gibt es provideRouter() in der ApplicationConfig — und loadComponent() für code-split lazy loading auf Component-Ebene.
// app.routes.ts
import { Routes } from '@angular/router' ;
export const routes: Routes = [
{
path: '' ,
// Eager-loaded Component (klein, immer benötigt)
loadComponent: () =>
import ('./pages/home/home.component' )
.then ((m) => m.HomeComponent ),
title: 'Home — SpockyMagicAI' ,
},
{
path: 'dashboard' ,
loadComponent: () =>
import ('./pages/dashboard/dashboard.component' )
.then ((m) => m.DashboardComponent ),
// Route-Guard als Funktion (kein Injectable nötig)
canActivate: [authGuard],
title: 'Dashboard' ,
},
{
path: 'admin' ,
// Child-Routes als eigenes Lazy-Bundle laden
loadChildren: () =>
import ('./pages/admin/admin.routes' )
.then ((m) => m.adminRoutes ),
canActivate: [authGuard, adminGuard],
},
{
path: 'user/:id' ,
loadComponent: () =>
import ('./pages/user/user.component' )
.then ((m) => m.UserComponent ),
// withComponentInputBinding() → Route-Params als @Input / input()
resolve: { user: userResolver },
},
{ path: '**' , redirectTo: '' },
];
// auth.guard.ts
import { inject } from '@angular/core' ;
import { CanActivateFn , Router } from '@angular/router' ;
import { AuthService } from '../services/auth.service' ;
export const authGuard : CanActivateFn = (route, state) => {
const auth = inject (AuthService );
const router = inject (Router );
if (auth.isLoggedIn ()) {
return true ;
}
// Redirect mit Return-URL
return router.createUrlTree (['/login' ], {
queryParams: { returnUrl: state.url },
});
};
// user.component.ts
import { Component, input, OnInit } from '@angular/core' ;
@Component ({
standalone: true ,
template: `<h1>User: {{ id() }}</h1>` ,
})
export class UserComponent {
// Route-Parameter automatisch als Signal via withComponentInputBinding()
id = input .required <string >();
}
@defer ist eine der mächtigsten neuen Angular-Features: Template-Blöcke werden erst bei Bedarf geladen und gerendert — mit deklarativer Kontrolle über Trigger und Fallbacks, direkt im Template ohne Code.
<!-- Grundstruktur -->
@defer (on viewport) {
<!-- Wird erst geladen wenn das Element in den Viewport kommt -->
<app-heavy-chart />
}
@loading (minimum 100ms; after 500ms) {
<!-- Shown während des Ladens (minimum 100ms, erst nach 500ms) -->
<div class="spinner">Lade Chart...</div>
}
@error {
<!-- Falls das Lazy-Bundle fehlschlägt -->
<p>Chart konnte nicht geladen werden.</p>
}
@placeholder (minimum 500ms) {
<!-- Platzhalterbefore Laden beginnt -->
<div class="chart-placeholder"></div>
}
<!-- 1. on viewport — lädt wenn Element sichtbar wird -->
@defer (on viewport) { <app-analytics /> }
<!-- 2. on interaction — lädt bei Klick/Fokus/Tastendruck -->
@defer (on interaction) { <app-comments /> }
<!-- 3. on hover — lädt bei Mouse-Over -->
@defer (on hover) { <app-tooltip-details /> }
<!-- 4. on immediate — lädt sofort (nach Main-Thread-Idle) -->
@defer (on immediate) { <app-sidebar /> }
<!-- 5. on timer — lädt nach Zeitverzögerung -->
@defer (on timer(3s)) { <app-cookie-banner /> }
<!-- 6. when — lädt wenn Signal-Bedingung true -->
@defer (when isUserLoggedIn()) { <app-dashboard /> }
<!-- 7. prefetch — vorladen, aber erst rendern wenn Trigger -->
@defer (on viewport; prefetch on idle) {
<app-heavy-modal />
}
@Component ({
standalone: true ,
template: `
<h2>Featured Products</h2>
@defer (on viewport; prefetch on idle) {
<app-product-grid [category]="selectedCategory()" />
}
@loading (after 200ms; minimum 500ms) {
<div class="skeleton-grid">
@for (i of skeletonItems; track i) {
<div class="skeleton-card"></div>
}
</div>
}
@error {
<p class="error">Produkte konnten nicht geladen werden.
<button (click)="retry()">Erneut versuchen</button>
</p>
}
@placeholder {
<div class="placeholder-hint">Scrollen Sie für Produkte</div>
}
` ,
})
export class ProductsPageComponent {
selectedCategory = signal ('all' );
skeletonItems = Array .from ({ length: 6 });
}
Claude Code generiert typsichere Angular-Komponenten, Standalone-Routing und Signal-basierte State-Management-Lösungen — direkt in deiner IDE.
Kostenlos testen →
In der Standalone-Welt wird HttpClient via provideHttpClient() bereitgestellt. Interceptors sind jetzt functional — keine Injectable-Klassen mehr, sondern einfache Funktionen die mit inject() Services nutzen.
// app.config.ts
import {
provideHttpClient,
withFetch, // Fetch API statt XMLHttpRequest
withInterceptors, // Functional Interceptors registrieren
withRequestedWith, // X-Requested-With Header
} from '@angular/common/http' ;
export const appConfig : ApplicationConfig = {
providers: [
provideHttpClient (
withFetch (),
withInterceptors ([authInterceptor, loggingInterceptor]),
),
],
};
// auth.interceptor.ts
import { HttpInterceptorFn , HttpRequest, HttpHandlerFn } from '@angular/common/http' ;
import { inject } from '@angular/core' ;
import { AuthService } from '../services/auth.service' ;
export const authInterceptor : HttpInterceptorFn = (
req: HttpRequest <unknown >,
next: HttpHandlerFn
) => {
const auth = inject (AuthService );
const token = auth.getToken ();
if (!token) {
return next (req);
}
const authReq = req.clone ({
setHeaders: {
Authorization: `Bearer ${token}` ,
'Content-Type' : 'application/json' ,
},
});
return next (authReq);
};
// logging.interceptor.ts
import { HttpInterceptorFn } from '@angular/common/http' ;
import { tap, catchError, throwError } from 'rxjs' ;
export const loggingInterceptor : HttpInterceptorFn = (req, next) => {
const start = Date .now ();
return next (req).pipe (
tap ({
complete: () => {
const duration = Date .now () - start;
console.log (`[HTTP] ${req.method} ${req.url} — ${duration}ms` );
},
}),
catchError ((error) => {
console.error (`[HTTP ERROR] ${req.url}:` , error);
return throwError (() => error);
})
);
};
// products.service.ts
import { Injectable, inject, signal } from '@angular/core' ;
import { HttpClient } from '@angular/common/http' ;
import { toSignal } from '@angular/core/rxjs-interop' ;
@Injectable ({ providedIn: 'root' })
export class ProductsService {
// inject() funktioniert in Klassen-Feldern direkt
private readonly http = inject (HttpClient );
private readonly baseUrl = '/api/products' ;
getProducts (category: string ) {
return this .http.get <Product []>(
` ${this .baseUrl}?category= ${category}`
);
}
createProduct (product: Omit <Product , 'id' >) {
return this .http.post <Product >(this .baseUrl, product);
}
updateProduct (id: number , changes: Partial <Product >) {
return this .http.patch <Product >(` ${this .baseUrl}/${id}` , changes);
}
}
Angular 17 führte built-in Control Flow ein, der *ngIf, *ngFor und *ngSwitch ersetzt. Die neue Syntax ist deklarativer, typsicherer und ermöglicht besseres Type-Narrowing ohne Boilerplate.
<!-- Vorher: *ngIf -->
<div *ngIf="user; else loading">{{ user.name }}</div>
<ng-template #loading>Lade...</ng-template>
<!-- Neu: @if mit automatischem Type Narrowing -->
@if (user) {
<!-- TypeScript weiß: user ist NICHT null/undefined hier -->
<div>{{ user.name }}</div>
<p>{{ user.email }}</p>
} @else if (isLoading()) {
<div class="spinner">Lade Benutzer...</div>
} @else {
<p>Kein Benutzer gefunden.</p>
}
<!-- Vorher: *ngFor -->
<li *ngFor="let item of items; trackBy: trackById">{{ item.name }}</li>
<!-- Neu: @for mit track (Pflicht!) -->
<ul>
@for (product of products(); track product.id) {
<li>
<strong>{{ product.name }}</strong>
<span>{{ product.price | currency:'EUR' }}</span>
<button (click)="addToCart(product)">Kaufen</button>
</li>
} @empty {
<!-- @empty block — wenn Array leer ist -->
<li>Keine Produkte in dieser Kategorie.</li>
}
</ul>
<!-- @for mit Loop-Variablen -->
@for (item of items(); track item.id; let i = $index, last = $last) {
<div [class.last]="last">{{ i + 1 }}. {{ item.name }}</div>
}
<!-- Vorher: [ngSwitch] -->
<div [ngSwitch]="status">
<div *ngSwitchCase="'loading'">...</div>
<div *ngSwitchDefault>...</div>
</div>
<!-- Neu: @switch -->
@switch (order.status) {
@case ('pending') {
<span class="badge pending">Ausstehend</span>
}
@case ('processing') {
<span class="badge processing">In Bearbeitung</span>
}
@case ('shipped') {
<span class="badge shipped">Versendet</span>
}
@case ('delivered') {
<span class="badge delivered">Zugestellt</span>
}
@default {
<span class="badge unknown">{{ order.status }}</span>
}
}
@Component ({
standalone: true ,
imports: [CurrencyPipe, DatePipe],
template: `
<h2>Produkte ({{ products().length }})</h2>
@if (isLoading()) {
<div class="spinner"></div>
} @else if (error()) {
<p class="error">{{ error() }}</p>
} @else {
<div class="grid">
@for (p of products(); track p.id) {
<div class="card">
<h3>{{ p.name }}</h3>
<span>{{ p.price | currency:'EUR' }}</span>
@switch (p.stock) {
@case (0) { <span class="out">Ausverkauft</span> }
@default { <span class="in">{{ p.stock }} verfügbar</span> }
}
</div>
} @empty {
<p>Keine Produkte gefunden.</p>
}
</div>
}
` ,
})
export class ProductListComponent {
products = signal <Product []>([]);
isLoading = signal (true );
error = signal <string | null >(null );
}
ng generate @angular/core:control-flowStandalone Components, Signals, @defer und modernes Control Flow — Claude Code versteht die aktuelle Angular-Architektur und generiert produktionsreifen Code für deine Projekte. Jetzt kostenlos testen und die ersten Standalone-Komponenten generieren lassen.
14 Tage kostenlos testen →