Angular Cheat Sheet
Updated for Angular v21 — includes Signals, Zoneless, Signal Forms & more.
Signals
Angulars reaktive Primitive — stabil seit v20.
import { signal, computed, effect, linkedSignal } from '@angular/core';
// Writable Signal
const count = signal(0);
count(); // lesen → 0
count.set(5); // setzen
count.update(n => n + 1); // updaten
// Computed Signal (read-only, automatisches Tracking)
const double = computed(() => count() * 2);
// Effect (Side-Effects bei Signal-Änderungen)
effect(() => {
console.log('Count changed:', count());
});
// LinkedSignal (beschreibbares Computed — stabil seit v20)
const pageSize = signal(10);
const userPageSize = linkedSignal(() => pageSize());
// Reagiert auf pageSize(), kann aber manuell überschrieben werden:
userPageSize.set(25); // überschreibt den abgeleiteten Wert
// Wenn pageSize() sich ändert → reset auf neuen abgeleiteten Wert
Resource (experimentell)
import { resource, httpResource } from '@angular/core';
// Async-Daten im Signal-Graph
const userId = signal(1);
const user = resource({
request: () => ({ id: userId() }), // wird getrackt
loader: async ({ request }) => {
const res = await fetch(`/api/users/${request.id}`);
return res.json();
}
});
user.value(); // Signal<User | undefined>
user.status(); // 'idle' | 'loading' | 'resolved' | 'error'
user.error(); // Fehler-Signal
// httpResource — reaktiver HTTP GET Wrapper (experimentell, seit v19.2)
const userData = httpResource<User>(() => `/api/users/${userId()}`);
// Varianten: httpResource.text(), httpResource.blob()
// Für POST/PUT/DELETE → weiterhin HttpClient verwenden
Control Flow (Template-Syntax)
Stabil seit v18. Ersetzt *ngIf, *ngFor, ngSwitch. Kein CommonModule nötig.
<!-- @if / @else if / @else -->
@if (user(); as u) {
<p>Hallo {{ u.name }}</p>
} @else if (loading()) {
<spinner />
} @else {
<p>Nicht angemeldet</p>
}
<!-- @for — track ist Pflicht! -->
@for (item of items(); track item.id) {
<li>{{ $index + 1 }}. {{ item.name }}</li>
} @empty {
<li>Keine Einträge</li>
}
<!-- Implizite Variablen: $index, $first, $last, $even, $odd, $count -->
<!-- @switch -->
@switch (status()) {
@case ('active') { <span class="green">Aktiv</span> }
@case ('inactive') { <span class="red">Inaktiv</span> }
@default { <span>Unbekannt</span> }
}
@defer (Lazy Loading im Template)
@defer (on viewport) {
<heavy-component />
} @placeholder {
<p>Scrollen zum Laden...</p>
} @loading (minimum 500ms) {
<spinner />
} @error {
<p>Laden fehlgeschlagen</p>
}
<!-- Trigger-Optionen -->
@defer (on idle) <!-- Default: wenn Browser idle -->
@defer (on viewport) <!-- wenn Element sichtbar wird -->
@defer (on interaction) <!-- bei Klick/Fokus auf Placeholder -->
@defer (on hover) <!-- bei Hover über Placeholder -->
@defer (on timer(3000)) <!-- nach 3 Sekunden -->
@defer (when condition()) <!-- wenn Signal/Expression true -->
Components (Signal-basiert)
Standalone ist seit v19 der Default — standalone: true ist implizit.
@Component({
selector: 'app-user-card',
template: `
<h2>{{ name() }}</h2>
<button (click)="clicked.emit()">Click</button>
<input [value]="search()" (input)="search.set($event.target.value)" />
`
})
export class UserCardComponent {
// Input als Signal (ersetzt @Input)
name = input.required<string>(); // Pflicht-Input
age = input(0); // Optional mit Default
label = input<string>(); // Optional, undefined möglich
// Output (ersetzt @Output)
clicked = output<void>(); // Emit: this.clicked.emit()
selected = output<User>(); // Emit: this.selected.emit(user)
// Two-Way Binding mit model()
search = model(''); // Eltern: <app [(search)]="query" />
// View Queries als Signals (ersetzt @ViewChild/@ViewChildren)
myDiv = viewChild<ElementRef>('myDiv'); // Signal<ElementRef | undefined>
myDiv = viewChild.required<ElementRef>('myDiv'); // Signal<ElementRef>
items = viewChildren(ItemComponent); // Signal<readonly ItemComponent[]>
// Content Queries
header = contentChild<TemplateRef<any>>('header');
tabs = contentChildren(TabComponent);
}
Lifecycle
// Klassisch (weiterhin verfügbar)
ngOnInit() ngOnDestroy()
ngOnChanges() ngAfterViewInit()
ngDoCheck() ngAfterContentInit()
// Neu: afterRender / afterNextRender (seit v17)
constructor() {
afterRender(() => {
// Läuft nach jedem Render-Zyklus
console.log('DOM updated');
});
afterNextRender(() => {
// Läuft einmal nach dem nächsten Render
this.chart = new Chart(this.canvas().nativeElement);
});
}
Dependency Injection
// inject() Funktion (empfohlen — ersetzt Constructor Injection)
export class UserService {
private http = inject(HttpClient);
private router = inject(Router);
private config = inject(APP_CONFIG, { optional: true });
}
// InjectionToken
export const APP_CONFIG = new InjectionToken<AppConfig>('app.config');
// Provider registrieren
bootstrapApplication(AppComponent, {
providers: [
{ provide: APP_CONFIG, useValue: { apiUrl: '/api' } },
{ provide: UserService, useClass: UserService },
{ provide: Logger, useFactory: () => new Logger(inject(Config)) },
]
});
Routing
// app.routes.ts
export const routes: Routes = [
{ path: '', component: HomeComponent },
// Lazy Loading
{
path: 'dashboard',
loadComponent: () => import('./dashboard.component')
.then(m => m.DashboardComponent),
canActivate: [authGuard]
},
// Lazy-loaded Child Routes
{
path: 'admin',
loadChildren: () => import('./admin/admin.routes')
.then(m => m.ADMIN_ROUTES),
canMatch: [adminGuard] // verhindert sogar Chunk-Download
},
// Redirect & Wildcard
{ path: 'home', redirectTo: '', pathMatch: 'full' },
{ path: '**', component: NotFoundComponent }
];
// App-Setup
provideRouter(
routes,
withComponentInputBinding(), // Route-Params → input() Signals
withViewTransitions(), // Native View Transitions API
withPreloading(PreloadAllModules)
)
Functional Guards
// Klassenbasierte Guards sind deprecated — funktionale verwenden!
export const authGuard: CanActivateFn = (route, state) => {
const auth = inject(AuthService);
if (auth.isLoggedIn()) return true;
return inject(Router).createUrlTree(['/login']);
};
// Resolve
export const userResolver: ResolveFn<User> = (route) => {
return inject(UserService).getUser(route.params['id']);
};
// CanDeactivate (z.B. ungespeicherte Änderungen)
export const unsavedGuard: CanDeactivateFn<EditComponent> = (component) => {
return component.hasUnsavedChanges()
? confirm('Ungespeicherte Änderungen verwerfen?')
: true;
};
HttpClient
Setup
provideHttpClient(
withInterceptors([authInterceptor, loggingInterceptor]),
withFetch(), // fetch API statt XMLHttpRequest
withXsrfConfiguration({ cookieName: 'XSRF-TOKEN' })
)
Requests
export class ApiService {
private http = inject(HttpClient);
// GET
getUsers() {
return this.http.get<User[]>('/api/users');
}
// POST
createUser(user: User) {
return this.http.post<User>('/api/users', user);
}
// PUT
updateUser(id: number, user: User) {
return this.http.put<User>(`/api/users/${id}`, user);
}
// DELETE
deleteUser(id: number) {
return this.http.delete(`/api/users/${id}`);
}
// Mit Optionen
search(query: string) {
return this.http.get<Result[]>('/api/search', {
params: { q: query },
headers: { 'X-Custom': 'value' },
observe: 'response' // vollständige HttpResponse
});
}
}
Functional Interceptors
// Auth-Token anhängen
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const token = inject(AuthService).getToken();
if (token) {
req = req.clone({
setHeaders: { Authorization: `Bearer ${token}` }
});
}
return next(req);
};
// Logging
export const loggingInterceptor: HttpInterceptorFn = (req, next) => {
const started = Date.now();
return next(req).pipe(
tap({ complete: () => console.log(`${req.url}: ${Date.now() - started}ms`) })
);
};
// Retry bei Fehler
export const retryInterceptor: HttpInterceptorFn = (req, next) => {
return next(req).pipe(retry({ count: 2, delay: 1000 }));
};
Forms
Reactive Forms
export class MyFormComponent {
private fb = inject(FormBuilder);
form = this.fb.group({
name: ['', [Validators.required, Validators.minLength(3)]],
email: ['', [Validators.required, Validators.email]],
age: [null as number | null, [Validators.min(0)]],
addresses: this.fb.array([this.createAddress()])
});
createAddress() {
return this.fb.group({
street: [''],
city: ['', Validators.required]
});
}
get addresses() {
return this.form.controls.addresses;
}
onSubmit() {
if (this.form.valid) {
console.log(this.form.value); // typsicher!
}
}
}
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<input formControlName="name" />
@if (form.controls.name.errors?.['required']) {
<span>Name ist Pflicht</span>
}
<input formControlName="email" type="email" />
<input formControlName="age" type="number" />
@for (addr of addresses.controls; track $index) {
<div [formGroup]="addr">
<input formControlName="street" />
<input formControlName="city" />
</div>
}
<button type="button" (click)="addresses.push(createAddress())">
Adresse hinzufügen
</button>
<button type="submit" [disabled]="form.invalid">Speichern</button>
</form>
Signal Forms (experimentell, v21)
import { form } from '@angular/forms/signal';
// Signal-basiertes Form — deklarativ und reaktiv
const myForm = form(mySignalModel);
// Automatische Synchronisation zwischen Signal-Daten und UI
// Deklarative Validierung
// API kann sich noch ändern!
Zoneless (Default seit v21)
Neue Apps verwenden kein zone.js mehr — Change Detection wird durch Signals getriggert.
// Neues Projekt (v21+): automatisch zoneless
bootstrapApplication(AppComponent, {
providers: [
provideZonelessChangeDetection(), // implizit in neuen v21 Projekten
]
});
// Was triggert Change Detection ohne Zone.js?
// ✅ Signal-Änderungen (.set(), .update())
// ✅ Template-Event-Bindings ((click), (input), etc.)
// ✅ Async Pipe
// ✅ markForCheck() / detectChanges()
// ❌ setTimeout/setInterval → manuell triggern oder Signals verwenden
// ❌ Promise.then() → Signals oder async pipe verwenden
App-Setup (Standalone, kein NgModule)
// main.ts
bootstrapApplication(AppComponent, {
providers: [
provideRouter(routes,
withComponentInputBinding(),
withViewTransitions()
),
provideHttpClient(
withInterceptors([authInterceptor]),
withFetch()
),
provideAnimationsAsync(),
provideZonelessChangeDetection(),
// Legacy NgModule einbinden
importProvidersFrom(SomeLegacyModule),
]
});
Quick Reference
Aufgabe API
──────────────────────────────── ────────────────────────────
Reaktiver State signal()
Abgeleiteter State computed()
Beschreibbarer abgeleiteter State linkedSignal()
Side-Effect effect()
Async-Daten (GET) httpResource() / resource()
Input-Property input() / input.required()
Output-Event output()
Two-Way Binding model()
DOM-Referenz viewChild() / viewChildren()
Service injizieren inject()
Lazy-Load Template @defer
Conditional Rendering @if / @else
Listen rendern @for (track!)
Route Guards CanActivateFn (funktional)
HTTP Interceptors HttpInterceptorFn (funktional)
Change Detection Zoneless (Signals)