Der offizielle Style Guide wurde 2025 (mit v20) neu geschrieben, und die CLI hat die Standardkonventionen geändert: Die Dateien haben keine Typ-Suffixe mehr (.component.ts, .service.ts), und die Klassen enden nicht mehr auf Component. Die Idee: Der Name beschreibt, was die Sache ist, nicht, was für ein Angular-Artefakt sie ist.
| Modern (CLI v20+, aktueller Style Guide) | Klassisch (mit Suffixen) | |
|---|---|---|
| Datei | user-profile-card.ts / .html / .scss | user-profile-card.component.ts |
| Klasse / Funktion | UserProfileCard | UserProfileCardComponent |
| Selector / Name | app-user-profile-card | app-user-profile-card |
| CLI-Befehl | ng generate component user-profile-card # ng g c … | |
Dateien
user-profile.ts ← die Komponente (Klasse UserProfile)
user-profile.html ← Template
user-profile.scss ← Styles
user-profile.spec.ts ← Test
derselbe Basisname = leicht zu finden, gemeinsam leicht zu verschieben
auth-store.ts ← Service mit Zustand
users-api.ts ← Service für den Datenzugriff
time-ago-pipe.ts ← Pipe (Klasse TimeAgoPipe)
auth-guard.ts ← funktionaler Guard (authGuard)Bestehende Projekte mit Suffixen können dabei bleiben. Entscheidend ist die Konsistenz. Wenn du die Suffixe für neu generierte Dateien behalten willst:
// Du bevorzugst die alten Suffixe (.component.ts, .service.ts)? Du kannst sie in der angular.json behalten:
{
"schematics": {
"@schematics/angular:component": { "type": "component" },
"@schematics/angular:directive": { "type": "directive" },
"@schematics/angular:service": { "type": "service" },
"@schematics/angular:guard": { "typeSeparator": "." },
"@schematics/angular:pipe": { "typeSeparator": "." }
}
}
// Wichtig ist, im ganzen Projekt KONSISTENT zu sein.Die Reihenfolge der Member in einer Komponente
@Component({
selector: 'app-user-profile',
imports: [Avatar, DatePipe],
templateUrl: './user-profile.html',
styleUrl: './user-profile.scss',
host: { '[class.compact]': 'compact()' },
})
export class UserProfile {
// 1. Die API der Komponente: Inputs, Models, Outputs (readonly)
readonly userId = input.required<number>();
readonly compact = input(false, { transform: booleanAttribute });
readonly edit = output<number>();
// 2. Queries (readonly)
private readonly nameInput = viewChild<ElementRef<HTMLInputElement>>('name');
// 3. Abhängigkeiten (private readonly)
private readonly users = inject(UsersStore);
// 4. Zustand und abgeleitete Werte — protected, wenn nur das Template sie nutzt
protected readonly user = computed(() => this.users.byId(this.userId()));
protected readonly editing = signal(false);
// 5. Methoden — benannt nach DEM, WAS SIE TUN, nicht nach dem Event, das sie auslöst
protected startEditing(): void {
this.editing.set(true);
}
}Kurze Regeln
// ✓ inject() statt Konstruktor
private readonly http = inject(HttpClient);
// ✓ protected für Member, die nur im Template verwendet werden (nicht Teil der öffentlichen API)
protected readonly fullName = computed(() => `${this.first()} ${this.last()}`);
// ✓ readonly für alles, was Angular initialisiert: input, output, model, viewChild, signal
readonly value = model(0);
// ✓ host: {} statt @HostBinding / @HostListener
host: { '[attr.aria-expanded]': 'open()', '(keydown.escape)': 'close()' }
// ✓ [class.x] / [style.x] statt ngClass / ngStyle
// ✓ @if / @for / @switch statt *ngIf / *ngFor / ngSwitch
// ✓ schlanke Lifecycle-Hooks: rufen Methoden mit klaren Namen auf
ngOnInit() { this.loadInitialFilters(); }Handler benennen
<!-- ✗ der Name beschreibt das Event -->
<button (click)="handleClick()">Speichern</button>
<input (keydown.enter)="onKeydown()" />
<!-- ✓ der Name beschreibt die Aktion -->
<button (click)="saveProfile()">Speichern</button>
<input (keydown.enter)="search()" />Zusammenfassung des Guides
| Regel | Warum |
|---|---|
| Ein Konzept pro Datei | Leicht zu finden, zu testen und zu verschieben. |
| Derselbe Name für .ts/.html/.scss/.spec.ts | Visuelle Gruppierung, schnelle Suche. |
| Nach Feature organisieren, nicht nach Typ | Was sich gemeinsam ändert, bleibt zusammen. |
Ein Präfix für Selectors (app-, adm-) | Keine Kollisionen mit nativen Elementen oder denen von Bibliotheken. |
inject() statt Konstruktor | Lesbar, funktioniert mit Vererbung und Funktionen. |
protected für Template-Member | Trennt die öffentliche API von Darstellungsdetails. |
readonly für Angular-Properties | Verhindert das versehentliche Überschreiben eines Signals. |
| Einfache Logik im Template | Berechnungen gehören in computed(). |
| Handler nach der Aktion benennen | saveProfile() sagt, was es tut; onClick() nicht. |
Wie du Dateien in Ordner gruppierst, zeigt Ordnerstruktur; wie du Typen und Modelle benennst, Interfaces & Modelle.
Selektoren und Präfixe
Komponenten haben Element-Selektoren in kebab-case mit dem Projektpräfix (app-user-card). Direktiven haben Attribut-Selektoren in camelCase mit demselben Präfix (appTooltip). Pipes haben camelCase-Namen ohne Präfix. Das Präfix wird einmal in angular.json gesetzt, und die CLI nutzt es bei jedem ng generate. Die Regeln @angular-eslint/component-selector und directive-selector prüfen das im Lint.
@Component({ selector: 'app-user-card' }) // Element, kebab-case, mit Präfix
@Directive({ selector: '[appTooltip]' }) // Attribut, camelCase, mit Präfix
@Pipe({ name: 'relativeTime' }) // camelCase, ohne Präfix
// angular.json → projects.<app>.prefix: "app"Automatische Migrationen zum modernen Stil
Du musst ein altes Projekt nicht von Hand umschreiben. Die Angular CLI hat Migrationen, die den Code selbst ändern, Datei für Datei. Alle diese gibt es in Angular 22.2:
ng generate @angular/core:standalone # NgModule → standalone
ng generate @angular/core:control-flow # *ngIf / *ngFor → @if / @for
ng generate @angular/core:inject # Konstruktor → inject()
ng generate @angular/core:signal-inputs # @Input → input()
ng generate @angular/core:outputs # @Output → output()
ng generate @angular/core:signal-queries # @ViewChild & Co. → viewChild()
ng generate @angular/core:signals # die drei oben, in einem Schritt
ng generate @angular/core:service # @Injectable → @Service (v22)
ng generate @angular/core:route-lazy-loading # component: → loadComponent
ng generate @angular/core:self-closing-tag # <app-x></app-x> → <app-x />
ng generate @angular/core:ngclass-to-class # [ngClass] → [class]
ng generate @angular/core:ngstyle-to-style # [ngStyle] → [style]
ng generate @angular/core:cleanup-unused-imports # ungenutzte Importe in KomponentenFühre sie einzeln auf einem sauberen Branch aus und prüfe nach jeder den Diff und die Tests. Die meisten akzeptieren --path, sodass du nur einen Ordner migrieren und schrittweise vorgehen kannst, Feature für Feature.
Häufige Stolperfallen
- Zwei Stile im selben Ordner gemischt (
user.component.tsnebenuser-card.ts): schlimmer als jeder der beiden, konsequent angewendet. - Eine riesige Migration in einem einzigen PR: Niemand kann sie reviewen. Geh Ordner für Ordner vor und trenne die Migration von Logikänderungen.
- Generische Namen (
utils.ts,helpers.ts,common.service.ts), die am Ende alles sammeln. Ein Name soll sagen, was die Datei tut.
Spickzettel
user-card.ts / .html / .css / .spec.ts ein Konzept pro Datei, ohne Typ-Suffix
class UserCard ohne Component am Ende
selector: app-user-card [appTooltip] Präfix auf Selektoren
inject() protected readonly in Komponenten
saveProfile() not onClick() Handler nach der Aktion benannt
ng generate @angular/core:<Migration>