Ghidul oficial de stil a fost rescris în 2025 (odată cu v20) și CLI-ul a schimbat convențiile implicite: fișierele nu mai au sufixe de tip (.component.ts, .service.ts), iar clasele nu mai au Component la final. Ideea: numele descrie ce e lucrul, nu ce tip de artefact Angular este.
| Modern (CLI v20+, style guide actual) | Clasic (cu sufixe) | |
|---|---|---|
| Fișier | user-profile-card.ts / .html / .scss | user-profile-card.component.ts |
| Clasă / funcție | UserProfileCard | UserProfileCardComponent |
| Selector / nume | app-user-profile-card | app-user-profile-card |
| Comandă CLI | ng generate component user-profile-card # ng g c … | |
Fișiere
user-profile.ts ← componenta (clasa UserProfile)
user-profile.html ← template
user-profile.scss ← stiluri
user-profile.spec.ts ← test
același nume de bază = ușor de găsit, ușor de mutat împreună
auth-store.ts ← serviciu cu stare
users-api.ts ← serviciu de acces la date
time-ago-pipe.ts ← pipe (clasa TimeAgoPipe)
auth-guard.ts ← guard funcțional (authGuard)Proiectele existente cu sufixe pot rămâne așa. Contează consecvența. Dacă vrei să păstrezi sufixele și pentru fișierele generate nou:
// Preferi sufixele vechi (.component.ts, .service.ts)? Le poți păstra din angular.json:
{
"schematics": {
"@schematics/angular:component": { "type": "component" },
"@schematics/angular:directive": { "type": "directive" },
"@schematics/angular:service": { "type": "service" },
"@schematics/angular:guard": { "typeSeparator": "." },
"@schematics/angular:pipe": { "typeSeparator": "." }
}
}
// Important e să fie CONSECVENT în tot proiectul.Ordinea membrilor într-o componentă
@Component({
selector: 'app-user-profile',
imports: [Avatar, DatePipe],
templateUrl: './user-profile.html',
styleUrl: './user-profile.scss',
host: { '[class.compact]': 'compact()' },
})
export class UserProfile {
// 1. API-ul componentei: 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. Dependențe (private readonly)
private readonly users = inject(UsersStore);
// 4. Stare și derivate — protected dacă le folosește doar template-ul
protected readonly user = computed(() => this.users.byId(this.userId()));
protected readonly editing = signal(false);
// 5. Metode — numite după CE FAC, nu după evenimentul care le declanșează
protected startEditing(): void {
this.editing.set(true);
}
}Reguli scurte
// ✓ inject() în loc de constructor
private readonly http = inject(HttpClient);
// ✓ protected pentru membrii folosiți doar în template (nu fac parte din API-ul public)
protected readonly fullName = computed(() => `${this.first()} ${this.last()}`);
// ✓ readonly pentru tot ce inițializează Angular: input, output, model, viewChild, signal
readonly value = model(0);
// ✓ host: {} în loc de @HostBinding / @HostListener
host: { '[attr.aria-expanded]': 'open()', '(keydown.escape)': 'close()' }
// ✓ [class.x] / [style.x] în loc de ngClass / ngStyle
// ✓ @if / @for / @switch în loc de *ngIf / *ngFor / ngSwitch
// ✓ hook-uri de lifecycle subțiri: apelează metode cu nume clare
ngOnInit() { this.loadInitialFilters(); }Numele handler-elor
<!-- ✗ numele descrie evenimentul -->
<button (click)="handleClick()">Salvează</button>
<input (keydown.enter)="onKeydown()" />
<!-- ✓ numele descrie acțiunea -->
<button (click)="saveProfile()">Salvează</button>
<input (keydown.enter)="search()" />Rezumatul ghidului
| Regulă | De ce |
|---|---|
| Un concept per fișier | Ușor de găsit, de testat, de mutat. |
| Același nume pentru .ts/.html/.scss/.spec.ts | Grupare vizuală, căutare rapidă. |
| Organizare pe feature, nu pe tip | Ce se schimbă împreună stă împreună. |
Prefix pe selectori (app-, adm-) | Fără coliziuni cu elemente native sau din librării. |
inject() în loc de constructor | Lizibil, merge cu moștenire și funcții. |
protected pentru membrii de template | Separă API-ul public de detaliile de afișare. |
readonly pentru proprietățile Angular | Previne suprascrierea accidentală a unui semnal. |
| Logică simplă în template | Calculele merg în computed(). |
| Handler-e numite după acțiune | saveProfile() spune ce face; onClick() nu. |
Cum grupezi fișierele în foldere e la Structura de foldere; cum denumești tipurile și modelele, la Interfețe & modele.
Selectori și prefixe
Componentele au selectori de element în kebab-case, cu prefixul proiectului (app-user-card). Directivele au selectori de atribut în camelCase, cu același prefix (appTooltip). Pipe-urile au nume în camelCase, fără prefix. Prefixul se setează o dată în angular.json, iar CLI-ul îl folosește la fiecare ng generate. Regulile @angular-eslint/component-selector și directive-selector le verifică în lint.
@Component({ selector: 'app-user-card' }) // element, kebab-case, cu prefix
@Directive({ selector: '[appTooltip]' }) // atribut, camelCase, cu prefix
@Pipe({ name: 'relativeTime' }) // camelCase, fără prefix
// angular.json → projects.<app>.prefix: "app"Migrații automate spre stilul modern
Nu trebuie să rescrii de mână un proiect vechi. Angular CLI are migrații care modifică singure codul, fișier cu fișier. Toate există în 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 # constructor → 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 # cele trei de mai sus, într-un singur pas
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 # importuri nefolosite din componenteRulează câte una, pe o ramură curată, și verifică diff-ul și testele după fiecare. Majoritatea acceptă --path, ca să migrezi doar un folder, deci poți merge treptat, feature cu feature.
Capcane frecvente
- Două stiluri amestecate în același folder (
user.component.tslângăuser-card.ts): e mai rău decât oricare dintre ele, aplicat consecvent. - O migrare uriașă într-un singur PR: nimeni nu o poate revizui. Mergi pe foldere și ține migrarea separată de schimbările de logică.
- Nume generice (
utils.ts,helpers.ts,common.service.ts) care ajung să adune orice. Un nume trebuie să spună ce face fișierul.
Fișă rapidă
user-card.ts / .html / .css / .spec.ts un concept per fișier, fără sufix de tip
class UserCard fără Component la final
selector: app-user-card [appTooltip] prefix pe selectori
inject() protected readonly în componente
saveProfile() not onClick() handler-e numite după acțiune
ng generate @angular/core:<migrare>