--- name: sdlc-angular-conventions description: | Angular 18-21 project structure, standalone components vs NgModule, control flow (@if/@for/@switch + *ngIf/*ngFor legacy), decorators, dependency injection (inject() function), lifecycle hooks, pipes, Angular Universal SSR pointer. Use this skill to: - Detect project style (standalone vs NgModule) and apply matching patterns. - Pick correct decorators and DI approach. - Use modern control flow (@if/@for/@switch) in Angular 17+ projects. - Apply `inject()` function over constructor injection where appropriate. - Wire bootstrap correctly (bootstrapApplication for standalone, AppModule for legacy). Do NOT use this skill for: - State management (see angular-state-and-rx). - Routing (see angular-routing). - Forms (see angular-forms). - Testing (see angular-testing). paths: ["src/**/*.ts", "src/**/*.html"] --- # Angular Conventions Modern Angular era (17+) with NgModule fallback for legacy projects. This skill covers structural patterns, decorators, DI, and template syntax. ## Project style detection | Markers | Style | |---|---| | `bootstrapApplication(AppComponent, {...})` in `main.ts` + NO `*.module.ts` (or only `app-routing.module.ts`) | **Standalone-first** (Angular 17+ recommended) | | `platformBrowserDynamic().bootstrapModule(AppModule)` + `app.module.ts` exists | **NgModule legacy** | | Both bootstrap calls or partial migration | **Mixed/migrating** — mirror per area; prefer standalone for new code | For new code in mixed projects, prefer standalone unless the team has a strict consistency rule. ## Project structure ``` src/ ├── main.ts # bootstrap ├── index.html ├── styles.scss ├── app/ │ ├── app.component.ts # root component │ ├── app.component.html │ ├── app.component.scss │ ├── app.config.ts # ApplicationConfig (standalone) │ ├── app.routes.ts # route definitions (standalone) │ ├── app.module.ts # AppModule (NgModule legacy) │ ├── app-routing.module.ts # routing module (NgModule legacy) │ ├── core/ # app-wide singletons │ │ ├── auth/ │ │ │ ├── auth.service.ts │ │ │ └── auth.guard.ts │ │ └── http/ │ │ └── auth.interceptor.ts │ ├── shared/ # cross-feature components/pipes/directives │ │ ├── components/ │ │ ├── pipes/ │ │ └── directives/ │ ├── features/ # feature folders │ │ ├── users/ │ │ │ ├── users.component.ts │ │ │ ├── users.component.html │ │ │ ├── users.component.scss │ │ │ ├── users.service.ts │ │ │ ├── users.routes.ts # feature routes │ │ │ └── user.model.ts │ │ └── orders/ │ └── layout/ # navbar, sidebar, app shell ├── assets/ └── environments/ ├── environment.ts └── environment.prod.ts ``` Mirror project layout. Don't restructure as part of feature work. ## Standalone bootstrap (Angular 17+) ```ts // main.ts import { bootstrapApplication } from '@angular/platform-browser'; import { provideRouter } from '@angular/router'; import { provideHttpClient, withInterceptors } from '@angular/common/http'; import { AppComponent } from './app/app.component'; import { routes } from './app/app.routes'; import { authInterceptor } from './app/core/http/auth.interceptor'; bootstrapApplication(AppComponent, { providers: [ provideRouter(routes), provideHttpClient(withInterceptors([authInterceptor])), ], }).catch((err) => console.error(err)); ``` Or split providers into `app.config.ts`: ```ts // app.config.ts import { ApplicationConfig } from '@angular/core'; import { provideRouter } from '@angular/router'; import { provideHttpClient, withInterceptors } from '@angular/common/http'; import { routes } from './app.routes'; import { authInterceptor } from './core/http/auth.interceptor'; export const appConfig: ApplicationConfig = { providers: [ provideRouter(routes), provideHttpClient(withInterceptors([authInterceptor])), ], }; // main.ts bootstrapApplication(AppComponent, appConfig); ``` `provide*` functions are the standalone-era replacements for `*Module` imports. Examples: `provideRouter`, `provideHttpClient`, `provideAnimations`, `provideStore` (NgRx). ## NgModule bootstrap (legacy) ```ts // main.ts import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { AppModule } from './app/app.module'; platformBrowserDynamic().bootstrapModule(AppModule); // app.module.ts @NgModule({ declarations: [AppComponent, UserListComponent, /* all components */], imports: [BrowserModule, AppRoutingModule, HttpClientModule, ReactiveFormsModule], providers: [{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }], bootstrap: [AppComponent], }) export class AppModule {} ``` Components and pipes must be declared in exactly one NgModule's `declarations` (NgModule legacy invariant). ## Standalone component ```ts import { Component, signal, computed, inject } from '@angular/core'; import { CommonModule } from '@angular/common'; import { RouterLink } from '@angular/router'; import { UsersService } from './users.service'; @Component({ selector: 'app-user-list', standalone: true, imports: [CommonModule, RouterLink], template: `

Users ({{ count() }})

@if (loading()) {

Loading...

} @else { } `, styleUrl: './user-list.component.scss', }) export class UserListComponent { private usersService = inject(UsersService); users = this.usersService.users; loading = this.usersService.loading; count = computed(() => this.users().length); } ``` `standalone: true` + explicit `imports` array. The component declares its own template dependencies (CommonModule for pipes, RouterLink, other standalone components, etc.). In Angular 19+, `standalone: true` is the default — you can omit it. For broad compatibility in 17/18, write it explicitly. ## NgModule component ```ts @Component({ selector: 'app-user-list', templateUrl: './user-list.component.html', styleUrl: './user-list.component.scss', }) export class UserListComponent { /* same body */ } // user.module.ts @NgModule({ declarations: [UserListComponent], imports: [CommonModule, RouterModule], exports: [UserListComponent], }) export class UserModule {} ``` For NgModule projects, declare every component, directive, pipe in some module. ## Decorators ```ts @Component({ ... }) // a Component (standalone or in NgModule) @Directive({ ... }) // a Directive (selector-based behavior) @Pipe({ name: 'capitalize', standalone: true }) // a Pipe transform @Injectable({ providedIn: 'root' }) // a service singleton at root injector @NgModule({ ... }) // a module (legacy) // Component members @Input() user!: User; // input property (decorator-based, legacy) @Input({ required: true }) user!: User; // required input (Angular 16+) @Input({ transform: trim }) name!: string; // input with transform @Output() delete = new EventEmitter(); @HostBinding('class.active') isActive = false; @HostListener('click', ['$event']) onClick(e: Event) {} @ViewChild('myRef') myRef!: ElementRef; @ContentChild(MyComponent) projected!: MyComponent; ``` For new code in Angular 17.1+, prefer signal-based `input()` / `output()` / `viewChild()`: ```ts import { input, output, viewChild } from '@angular/core'; user = input.required(); // required input as InputSignal name = input('Anonymous'); // optional with default delete = output(); // OutputEmitterRef myRef = viewChild('myRef'); // Signal ``` Mirror existing project — if all components use `@Input()`/`@Output()` decorators, follow that. New greenfield code: prefer signal-based APIs. ## Dependency injection ### `inject()` function (Angular 14.1+) ```ts import { inject, Injectable } from '@angular/core'; @Injectable({ providedIn: 'root' }) export class UsersService { private http = inject(HttpClient); private router = inject(Router); loadUsers() { return this.http.get('/api/users'); } } ``` `inject()` works inside: - `@Injectable` services (constructor or class field initializers). - `@Component` constructor or class field initializers. - Route guards / resolvers (functional API). - Factory providers. - `runInInjectionContext()` blocks. ### Constructor injection (legacy, still works) ```ts @Injectable({ providedIn: 'root' }) export class UsersService { constructor(private http: HttpClient, private router: Router) {} } ``` Both work. `inject()` is preferred for new code due to: - Works outside constructors (route guards, factory functions). - Better type narrowing with generic tokens. - Less boilerplate. ### Custom providers ```ts // Standalone (in providers array of bootstrapApplication or component) { provide: API_URL, useValue: 'https://api.example.com' } { provide: 'CONFIG', useFactory: () => ({ retries: 3 }) } { provide: AuditLog, useClass: ProductionAuditLog } // InjectionToken for type safety import { InjectionToken } from '@angular/core'; export const API_URL = new InjectionToken('API_URL'); // Inject private apiUrl = inject(API_URL); ``` ### Hierarchical injection `providedIn: 'root'` — singleton at root injector (most services). `providedIn: 'platform'` — shared across multiple Angular apps (rare). Component-level providers in `@Component({ providers: [...] })` — new instance per component instance. ## Modern control flow (Angular 17+) ```html @if (user(); as u) {

Welcome, {{ u.name }}

} @else if (loading()) {

Loading...

} @else { Log in } @for (item of items(); track item.id; let i = $index, isFirst = $first) {
  • {{ i + 1 }}. {{ item.name }}
  • } @empty {
  • No items
  • } @switch (status()) { @case ('loading') { } @case ('error') { } @default { } } ``` `track` is **mandatory** in `@for` — pick a stable identifier (entity ID), not index. Use `track $index` only when items have no stable identity. `$index`, `$first`, `$last`, `$even`, `$odd`, `$count` are available local variables in `@for`. ## Legacy structural directives (NgModule projects, Angular ≤16) ```html

    Welcome, {{ user.name }}

    Log in
  • {{ item.name }}
  • ``` Requires `CommonModule` import. ## Lifecycle hooks ```ts implements OnInit, OnDestroy, OnChanges, AfterViewInit ngOnInit() { /* after first @Input() values bound, before view */ } ngOnDestroy() { /* cleanup */ } ngOnChanges(changes: SimpleChanges) { /* @Input changes — replaces by signal `effect` */ } ngAfterViewInit() { /* DOM available */ } ``` Order: 1. Constructor (DI runs). 2. `ngOnChanges` (first call, with @Input values). 3. `ngOnInit`. 4. `ngDoCheck`. 5. `ngAfterContentInit`, `ngAfterContentChecked`. 6. `ngAfterViewInit`, `ngAfterViewChecked`. For destruction: `ngOnDestroy`. In standalone components with signals + `takeUntilDestroyed()`, manual `ngOnDestroy` is rarely needed. ## Pipes ```ts // Built-in {{ user.name | uppercase }} {{ user.createdAt | date:'short' }} {{ price | currency:'USD' }} {{ data | json }} {{ user$ | async }} // subscribe + unsubscribe automatic // Custom standalone pipe @Pipe({ name: 'truncate', standalone: true }) export class TruncatePipe implements PipeTransform { transform(value: string, max = 50): string { return value.length > max ? value.slice(0, max) + '...' : value; } } // Use {{ description | truncate:100 }} ``` ## Angular Universal (SSR) — pointer only For Angular 17+: `@angular/ssr` package + `provideClientHydration()` + `provideServerRendering()`. Out of scope for v0.0.1 of this plugin (SPA-focused). If BA spec requires SSR, flag in BLOCKERS — needs separate spec or follow-up plugin. For legacy: `@nguniversal/express-engine` — older approach. ## Anti-patterns - ❌ `*ngIf`/`*ngFor`/`*ngSwitch` in Angular 17+ standalone projects — use `@if`/`@for`/`@switch`. - ❌ Forgetting `track` in `@for` — Angular throws compile error in 17+. - ❌ Using index as `track` for reorderable lists — defeats the optimization. - ❌ Mixing standalone + NgModule arbitrarily — pick one per area. - ❌ `new MyService()` outside test files — bypasses DI. - ❌ Mutating `@Input()` / `input()` values directly — emit event for parent updates. - ❌ `subscribe()` without unsubscription strategy — leaks. - ❌ Logic in templates beyond simple expressions — extract to `computed()` or method. - ❌ Calling signal without parens (`*ngIf="users"` instead of `*ngIf="users()"`) — common bug. - ❌ `bypassSecurityTrustHtml` without justified upstream sanitization. - ❌ Reading `process.env` in components — use `environment.ts` (still PUBLIC after build, but consistent).