Angular & Frontend

Angular Standalone Components mit Claude Code 2026

Angular Standalone Components für moderne, modulfreie Apps — Claude Code baut Signals, Standalone Routing, Deferred Views und HttpClient ohne NgModule.

📅 6. Mai 2026 ⏱ 11 min Lesezeit 🏷 Angular 17+

Inhaltsverzeichnis

  1. Standalone Setup — bootstrapApplication & ApplicationConfig
  2. Signals — signal(), computed(), effect() und RxJS-Integration
  3. Standalone Routing — Lazy Loading mit loadComponent()
  4. Deferred Loading — @defer, @loading, @placeholder
  5. HttpClient und functional Interceptors
  6. Control Flow — @if, @for, @switch statt Direktiven

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.

Dieser Guide basiert auf Angular 17 und 18 mit dem neuen Control Flow, Signal APIs und der vollständigen Standalone-Architektur. Alle Codebeispiele laufen ohne NgModule.

Angular 1. Standalone Setup — bootstrapApplication, provideRouter, provideHttpClient

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 — Der neue Einstiegspunkt

// 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 — ApplicationConfig

// 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(),
  ],
};

AppComponent als Standalone Component

// 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 {}

Standalone-Vorteile auf einen Blick

Signal 2. Signals — signal(), computed(), effect() und RxJS-Integration

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.

Grundlegende Signal-APIs

// 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); }
}

Signal Input und Output (Angular 17.1+)

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());
  }
}

toSignal() und toObservable() — RxJS-Bridge

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 APIBeschreibungUse Case
signal(value)Schreibbares Signal erstellenLokaler State
computed(() => ...)Abgeleiteter, gecachter WertBerechneter State
effect(() => ...)Seiteneffekt bei ÄnderungLogging, DOM-Sync
input()Signal-basierter @Input()Component-Props
output()Signal-basierter @Output()Event-Emission
model()Two-Way-Signal-BindingFormulare
toSignal(obs$)Observable → SignalRxJS-Integration
toObservable(sig)Signal → ObservableRxJS-Pipelines

Router 3. Standalone Routing — loadComponent() und lazy Loading

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 — Vollständiges Routing-Setup

// 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: '' },
];

Functional Guards — kein Injectable Class

// 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 },
  });
};

Route-Params als Signal-Input via withComponentInputBinding()

// 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 4. Deferred Loading — @defer, @loading, @error, @placeholder

@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 eines @defer-Blocks

<!-- 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>
}

Alle @defer Trigger

<!-- 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 />
}

Praxisbeispiel: Lazy Product-List mit Prefetch

@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 });
}

Angular-Projekte mit Claude Code automatisieren

Claude Code generiert typsichere Angular-Komponenten, Standalone-Routing und Signal-basierte State-Management-Lösungen — direkt in deiner IDE.

Kostenlos testen →

HTTP 5. HttpClient und functional Interceptors

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.

provideHttpClient mit Features

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

Functional Auth-Interceptor

// 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 mit Error-Handling

// 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);
    })
  );
};

Service mit inject() statt Constructor-Injection

// 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);
  }
}

Control Flow 6. Control Flow — @if, @for, @switch statt ngIf/ngFor

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.

@if — Konditionelles Rendering mit Type Narrowing

<!-- 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>
}

@for — mit obligatorischem track für Performance

<!-- 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>
}

@switch — Typsicheres Multi-Branch

<!-- 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>
  }
}

Vollständiges Beispiel — Produktliste mit allem

@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);
}

Migration von ngIf/ngFor/ngSwitch

Claude Code für deine Angular-Projekte

Standalone 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 →