UNPKG

create-saltic

Version:

Spec-Driven Development (SDD) framework for constitutional software development - inject into any project

1,767 lines (1,545 loc) 71.2 kB
# Angular Development Constitution ## Core Principles ### I. Standalone Architecture Every Angular application must use standalone components over NgModules. All components should be designed with the `standalone` property as the default behavior without explicit declaration. #### Code Examples **✅ Correct: Standalone Component** ```typescript import { Component } from '@angular/core'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-standalone-example', standalone: true, imports: [CommonModule], template: ` <div *ngIf="isVisible"> <p>Hello, Standalone Component!</p> </div> @for (item of items; track item.id) { <div>{{item.name}}</div> } ` }) export class StandaloneExampleComponent { isVisible = true; items = [ { id: 1, name: 'Item 1' }, { id: 2, name: 'Item 2' } ]; } ``` **✅ Correct: Standalone Application Bootstrap** ```typescript // main.ts import { bootstrapApplication } from '@angular/platform-browser'; import { provideRouter } from '@angular/router'; import { provideHttpClient } from '@angular/common/http'; import { AppComponent } from './app/app.component'; bootstrapApplication(AppComponent, { providers: [ provideRouter([]), provideHttpClient() ] }); ``` **❌ Incorrect: NgModule-based Component** ```typescript // Avoid this pattern @Component({ selector: 'app-ngmodule-example', // No standalone property - defaults to false template: '<p>Legacy NgModule Component</p>' }) export class NgModuleExampleComponent {} @NgModule({ declarations: [NgModuleExampleComponent], imports: [CommonModule], exports: [NgModuleExampleComponent] }) export class LegacyModule {} ``` ### II. Signal-Based State Management All component state must be managed using signals for local state and `computed()` for derived state. State transformations must be pure and predictable. The `mutate` method on signals is forbidden; use `update` or `set` instead. #### Code Examples **✅ Correct: Signal-based Component State** ```typescript import { Component, signal, computed, input } from '@angular/core'; @Component({ selector: 'app-user-profile', standalone: true, template: ` <div> <h2>{{userName()}}</h2> <p>Age: {{userAge()}}</p> <p>Status: {{userStatus()}}</p> <p>Is Adult: {{isAdult()}}</p> <button (click)="incrementAge()">Birthday</button> <button (click)="toggleStatus()">Toggle Status</button> </div> ` }) export class UserProfileComponent { // Signal-based local state userName = signal('John Doe'); userAge = signal(25); isActive = signal(true); // Input signal for component communication userId = input.required<string>(); // Computed derived state userStatus = computed(() => this.isActive() ? 'Active' : 'Inactive'); isAdult = computed(() => this.userAge() >= 18); incrementAge() { // Use update() for transformations this.userAge.update(age => age + 1); } toggleStatus() { // Use set() for simple assignments this.isActive.set(!this.isActive()); } } ``` **✅ Correct: Input and Output Signals** ```typescript import { Component, input, output, EventEmitter } from '@angular/core'; @Component({ selector: 'app-product-card', standalone: true, template: ` <div class="product-card"> <h3>{{product().name}}</h3> <p>Price: ${{product().price}}</p> <button (click)="addToCart()">Add to Cart</button> </div> ` }) export class ProductCardComponent { // Input signals product = input.required<{ name: string; price: number }>(); discount = input<number>(0); // Output signal cartUpdated = output<{ product: string; quantity: number }>(); // Computed signal for final price finalPrice = computed(() => { const product = this.product(); const discount = this.discount(); return product.price * (1 - discount / 100); }); addToCart() { this.cartUpdated.emit({ product: this.product().name, quantity: 1 }); } } ``` **❌ Incorrect: Traditional Property-based State** ```typescript // Avoid this pattern @Component({ selector: 'app-traditional-state', template: ` <div>{{userName}}</div> <button (click)="updateName()">Update</button> ` }) export class TraditionalStateComponent { userName: string = 'John Doe'; updateName() { this.userName = 'Jane Doe'; // Direct property mutation } } ``` ### III. Performance-First Design Components must be optimized with `ChangeDetectionStrategy.OnPush`. All static images must use `NgOptimizedImage` (except for inline base64 images). Lazy loading must be implemented for all feature routes. #### Code Examples **✅ Correct: OnPush Change Detection** ```typescript import { Component, ChangeDetectionStrategy, input, signal } from '@angular/core'; @Component({ selector: 'app-performance-card', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgOptimizedImage], template: ` <div class="card"> <h3>{{title()}}</h3> <img ngSrc="/assets/images/{{imagePath()}}" width="300" height="200" priority="high" alt="Card image" /> <p>{{description()}}</p> </div> ` }) export class PerformanceCardComponent { title = input.required<string>(); description = input.required<string>(); imagePath = input.required<string>(); // Signal-based state works perfectly with OnPush isHovered = signal(false); onMouseEnter() { this.isHovered.set(true); } onMouseLeave() { this.isHovered.set(false); } } ``` **✅ Correct: Lazy Loaded Routes** ```typescript // app.routes.ts import { Routes } from '@angular/router'; export const routes: Routes = [ { path: '', loadComponent: () => import('./home/home.component').then(m => m.HomeComponent) }, { path: 'products', loadChildren: () => import('./products/products.routes').then(m => m.productsRoutes) }, { path: 'user', loadComponent: () => import('./user/user.component').then(m => m.UserComponent) } ]; // products.routes.ts export const productsRoutes: Routes = [ { path: '', loadComponent: () => import('./product-list/product-list.component').then(m => m.ProductListComponent) }, { path: ':id', loadComponent: () => import('./product-detail/product-detail.component').then(m => m.ProductDetailComponent) } ]; ``` **✅ Correct: Optimized Images** ```typescript import { Component, NgOptimizedImage } from '@angular/common'; @Component({ selector: 'app-optimized-images', standalone: true, imports: [NgOptimizedImage], template: ` <!-- High priority hero image --> <img ngSrc="/assets/hero.jpg" width="1200" height="600" priority="high" alt="Hero banner" /> <!-- Regular images --> <img ngSrc="/assets/product1.jpg" width="300" height="300" alt="Product 1" /> <!-- Responsive image --> <img ngSrc="/assets/responsive.jpg" width="800" height="400" sizes="(max-width: 768px) 100vw, 50vw" alt="Responsive image" /> ` }) export class OptimizedImagesComponent {} ``` **❌ Incorrect: Default Change Detection** ```typescript // Avoid this pattern @Component({ selector: 'app-default-change-detection', changeDetection: ChangeDetectionStrategy.Default, // Inefficient template: ` <img src="/assets/large-image.jpg" alt="Large image" /> <div *ngIf="data"> <!-- Complex template logic --> </div> ` }) export class DefaultChangeDetectionComponent { data: any; // This will trigger change detection on every property change updateData() { this.data = { ...this.data, timestamp: Date.now() }; } } ``` ### IV. Reactive Programming All applications must use Reactive forms instead of Template-driven forms. The async pipe must be used to handle observables. Native control flow (`@if`, `@for`, `@switch`) must be used instead of structural directives (`*ngIf`, `*ngFor`, `*ngSwitch`). #### Code Examples **✅ Correct: Reactive Forms with Async Pipe** ```typescript import { Component, inject } from '@angular/core'; import { CommonModule } from '@angular/common'; import { ReactiveFormsModule, FormBuilder, Validators } from '@angular/forms'; import { HttpClient } from '@angular/common/http'; import { Observable } from 'rxjs'; @Component({ selector: 'app-reactive-form', standalone: true, imports: [CommonModule, ReactiveFormsModule], template: ` <form [formGroup]="userForm" (ngSubmit)="onSubmit()"> <div> <label for="name">Name:</label> <input id="name" formControlName="name" type="text"> @if (userForm.controls.name.invalid && userForm.controls.name.touched) { <div class="error">Name is required</div> } </div> <div> <label for="email">Email:</label> <input id="email" formControlName="email" type="email"> </div> <div> <label for="role">Role:</label> <select id="role" formControlName="role"> @for (role of roles$ | async; track role.id) { <option [value]="role.id">{{role.name}}</option> } </select> </div> <button type="submit" [disabled]="userForm.invalid">Submit</button> </form> @if (submissionStatus$ | async; as status) { <div class="status" [class.success]="status.success" [class.error]="!status.success"> {{status.message}} </div> } ` }) export class ReactiveFormComponent { private fb = inject(FormBuilder); private http = inject(HttpClient); userForm = this.fb.group({ name: ['', Validators.required], email: ['', [Validators.required, Validators.email]], role: ['', Validators.required] }); roles$: Observable<Array<{ id: string; name: string }>>; submissionStatus$ = new Observable<{ success: boolean; message: string }>(); constructor() { this.roles$ = this.http.get<Array<{ id: string; name: string }>>('/api/roles'); } onSubmit() { if (this.userForm.valid) { this.submissionStatus$ = this.http.post<{ success: boolean; message: string }>( '/api/users', this.userForm.value ); } } } ``` **✅ Correct: Native Control Flow** ```typescript import { Component, signal } from '@angular/core'; @Component({ selector: 'app-native-control-flow', standalone: true, template: ` <!-- @if instead of *ngIf --> @if (isLoading()) { <div class="loading">Loading...</div> } @else if (error()) { <div class="error">{{error()}}</div> } @else { <div class="content"> <h3>{{user().name}}</h3> <p>{{user().email}}</p> </div> } <!-- @for instead of *ngFor --> <ul> @for (item of items(); track item.id) { <li [class.active]="item.isActive"> {{item.name}} ({{item.id}}) </li> } </ul> <!-- @switch instead of *ngSwitch --> @switch (userStatus()) { @case ('active') { <div class="status-active">User is active</div> } @case ('pending') { <div class="status-pending">User is pending</div> } @default { <div class="status-inactive">User is inactive</div> } } ` }) export class NativeControlFlowComponent { isLoading = signal(true); error = signal<string | null>(null); user = signal({ name: '', email: '' }); userStatus = signal<'active' | 'pending' | 'inactive'>('active'); items = signal([ { id: 1, name: 'Item 1', isActive: true }, { id: 2, name: 'Item 2', isActive: false }, { id: 3, name: 'Item 3', isActive: true } ]); } ``` **❌ Incorrect: Template-driven Forms and Structural Directives** ```typescript // Avoid this pattern @Component({ selector: 'app-template-driven-form', template: ` <!-- Template-driven form (avoid) --> <form #userForm="ngForm" (ngSubmit)="onSubmit(userForm)"> <input name="name" ngModel required #name="ngModel"> <div *ngIf="name.invalid && name.touched">Name is required</div> <!-- Structural directives (avoid) --> <div *ngIf="isLoading; else loadingElse"> <ul> <li *ngFor="let item of items; trackBy: trackById">{{item.name}}</li> </ul> <div [ngSwitch]="userStatus"> <div *ngSwitchCase="'active'">Active</div> <div *ngSwitchCase="'pending'">Pending</div> <div *ngSwitchDefault>Inactive</div> </div> </div> <ng-template #loadingElse> <div>Loading...</div> </ng-template> </form> ` }) export class TemplateDrivenFormComponent { isLoading = false; items = []; userStatus = 'active'; onSubmit(form: any) { // Template-driven form handling } trackById(index: number, item: any) { return item.id; } } ``` ### V. Single Responsibility Every component, service, and module must maintain a single, clear responsibility. Components must be kept small and focused. Services must be designed around a single responsibility with `providedIn: 'root'` for singleton services. #### Code Examples **✅ Correct: Single Responsibility Components** ```typescript // UserAvatarComponent - Only responsible for displaying user avatar @Component({ selector: 'app-user-avatar', standalone: true, template: ` <img [src]="userAvatar()" [alt]="userName()" class="avatar" /> ` }) export class UserAvatarComponent { userAvatar = input.required<string>(); userName = input.required<string>(); } // UserInfoComponent - Only responsible for displaying user info @Component({ selector: 'app-user-info', standalone: true, imports: [UserAvatarComponent], template: ` <div class="user-info"> <app-user-avatar [userAvatar]="user().avatar" [userName]="user().name" /> <div class="details"> <h3>{{user().name}}</h3> <p>{{user().email}}</p> <p>Joined: {{user().joinDate | date:'medium'}}</p> </div> </div> ` }) export class UserInfoComponent { user = input.required<{ name: string; email: string; avatar: string; joinDate: Date }>(); } // UserActionsComponent - Only responsible for user actions @Component({ selector: 'app-user-actions', standalone: true, template: ` <div class="user-actions"> <button (click)="editUser.emit()">Edit</button> <button (click)="deleteUser.emit()">Delete</button> <button (click)="viewProfile.emit()">View Profile</button> </div> ` }) export class UserActionsComponent { editUser = output<void>(); deleteUser = output<void>(); viewProfile = output<void>(); } ``` **✅ Correct: Single Responsibility Services** ```typescript // UserService - Only responsible for user data operations @Injectable({ providedIn: 'root' }) export class UserService { private http = inject(HttpClient); getUsers(): Observable<User[]> { return this.http.get<User[]>('/api/users'); } getUserById(id: string): Observable<User> { return this.http.get<User>(`/api/users/${id}`); } createUser(user: CreateUserDto): Observable<User> { return this.http.post<User>('/api/users', user); } updateUser(id: string, user: UpdateUserDto): Observable<User> { return this.http.put<User>(`/api/users/${id}`, user); } deleteUser(id: string): Observable<void> { return this.http.delete<void>(`/api/users/${id}`); } } // AuthService - Only responsible for authentication @Injectable({ providedIn: 'root' }) export class AuthService { private http = inject(HttpClient); login(credentials: LoginCredentials): Observable<AuthResponse> { return this.http.post<AuthResponse>('/api/auth/login', credentials); } logout(): Observable<void> { return this.http.post<void>('/api/auth/logout', {}); } refreshToken(): Observable<AuthResponse> { return this.http.post<AuthResponse>('/api/auth/refresh', {}); } forgotPassword(email: string): Observable<void> { return this.http.post<void>('/api/auth/forgot-password', { email }); } } // NotificationService - Only responsible for notifications @Injectable({ providedIn: 'root' }) export class NotificationService { notifications = signal<Notification[]>([]); showSuccess(message: string): void { this.notifications.update(notifs => [ ...notifs, { id: Date.now(), type: 'success', message } ]); } showError(message: string): void { this.notifications.update(notifs => [ ...notifs, { id: Date.now(), type: 'error', message } ]); } removeNotification(id: number): void { this.notifications.update(notifs => notifs.filter(n => n.id !== id)); } clearAll(): void { this.notifications.set([]); } } ``` **✅ Correct: Composite Component Using Single Responsibility Components** ```typescript @Component({ selector: 'app-user-profile', standalone: true, imports: [UserInfoComponent, UserActionsComponent], template: ` <div class="user-profile"> <app-user-info [user]="user()" /> <app-user-actions (editUser)="onEdit()" (deleteUser)="onDelete()" (viewProfile)="onViewProfile()" /> </div> ` }) export class UserProfileComponent { user = signal<User | null>(null); private userService = inject(UserService); constructor() { this.loadUser(); } private loadUser(): void { this.userService.getCurrentUser().subscribe(user => { this.user.set(user); }); } onEdit(): void { // Handle edit action } onDelete(): void { // Handle delete action } onViewProfile(): void { // Handle view profile action } } ``` **❌ Incorrect: Multi-Responsibility Component** ```typescript // Avoid this pattern @Component({ selector: 'app-monolith-component', template: ` <!-- Handles user display, actions, notifications, and data fetching --> <div *ngIf="user"> <img [src]="user.avatar" [alt]="user.name"> <div>{{user.name}} - {{user.email}}</div> <button (click)="editUser()">Edit</button> <button (click)="deleteUser()">Delete</button> <div *ngFor="let notification of notifications"> {{notification.message}} </div> <div *ngIf="isLoading">Loading...</div> </div> ` }) export class MonolithComponent { user: User | null = null; notifications: Notification[] = []; isLoading = false; constructor( private http: HttpClient, private authService: AuthService, private notificationService: NotificationService ) {} ngOnInit() { this.loadUser(); this.loadNotifications(); } loadUser() { this.isLoading = true; this.http.get<User>('/api/users/current').subscribe(user => { this.user = user; this.isLoading = false; }); } loadNotifications() { this.http.get<Notification[]>('/api/notifications').subscribe(notifications => { this.notifications = notifications; }); } editUser() { // Directly handles user editing } deleteUser() { // Directly handles user deletion } } ``` ### VI. Context7 Documentation Integration All Angular development must integrate context7 tools for external library documentation and reference management. This ensures that all technical decisions are based on current, authoritative documentation. #### Documentation Requirements **✅ Correct: Context7 Integration in Planning** ```typescript // During feature planning, always use context7 to resolve library references const libraryId = await mcp__context7__resolve-library-id('angular'); const docs = await mcp__context7__get-library-docs(libraryId, { topic: 'signals', tokens: 5000 }); // Document all library references with their context7-compatible IDs /** * Angular Signals Implementation * @library /angular/angular * @version 17.0.0 * @context7Id /angular/angular/v17.0.0 */ ``` **✅ Correct: Library Reference Documentation** ```typescript // Import statements should include context7 references /** * @import { signal, computed } from '@angular/core' * @library /angular/angular * @context7Id /angular/angular/v17.0.0 * @docsRetrieved 2025-01-15 */ @Component({ selector: 'app-signal-example', standalone: true }) export class SignalExampleComponent { // Use signals with proper documentation count = signal(0); doubleCount = computed(() => this.count() * 2); } ``` #### Context7 Integration Standards - **Library Resolution**: Always use `mcp__context7__resolve-library-id` before implementing external library features - **Documentation Retrieval**: Use `mcp__context7__get-library-docs` with specific topics and token limits - **Reference Tracking**: Document all context7-compatible library IDs in component/service documentation - **Version Awareness**: Ensure retrieved documentation matches the project's library versions - **Update Monitoring**: Regularly refresh documentation to stay current with library updates #### Planning Integration When planning Angular features: 1. Identify all external libraries and frameworks referenced in the feature specification 2. Use context7 tools to retrieve current documentation for each library 3. Document all context7-compatible library IDs in the planning artifacts 4. Ensure implementation follows the latest documented patterns and best practices 5. Update documentation references when library versions change ## Technical Standards ### Component Standards - Use `input()` and `output()` functions instead of decorators - Set `changeDetection: ChangeDetectionStrategy.OnPush` in `@Component` decorator - Prefer inline templates for small components - Do NOT use `ngClass`, use `class` bindings instead - Do NOT use `ngStyle`, use `style` bindings instead - Do NOT use the `@HostBinding` and `@HostListener` decorators. Put host bindings inside the `host` object of the `@Component` or `@Directive` decorator instead #### Code Examples **✅ Correct: Modern Component with Signal Inputs/Outputs** ```typescript import { Component, input, output, signal, ChangeDetectionStrategy, computed } from '@angular/core'; @Component({ selector: 'app-user-card', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgOptimizedImage], template: ` <div class="user-card" [class.active]="isActive()" [style.border-color]="borderColor()"> <img ngSrc="{{user().avatar}}" [alt]="user().name" width="80" height="80" class="avatar" /> <div class="user-info"> <h3>{{user().name}}</h3> <p>{{user().email}}</p> <p>Status: {{userStatus()}}</p> </div> <div class="actions"> <button (click)="edit.emit()">Edit</button> <button (click)="delete.emit()">Delete</button> </div> </div> `, host: { '[attr.aria-label]': "'User card for ' + user().name", '(mouseenter)': 'onMouseEnter()', '(mouseleave)': 'onMouseLeave()' } }) export class UserCardComponent { // Signal inputs instead of @Input() user = input.required<{ name: string; email: string; avatar: string; status: string }>(); isActive = input<boolean>(false); // Signal output instead of @Output() edit = output<void>(); delete = output<void>(); // Signal-based local state isHovered = signal(false); // Computed signal for derived state userStatus = computed(() => { const status = this.user().status; return status.charAt(0).toUpperCase() + status.slice(1); }); borderColor = computed(() => this.isHovered() ? '#007bff' : '#dee2e6'); onMouseEnter() { this.isHovered.set(true); } onMouseLeave() { this.isHovered.set(false); } } ``` **✅ Correct: Class and Style Bindings (No ngClass/ngStyle)** ```typescript import { Component, signal, ChangeDetectionStrategy } from '@angular/core'; @Component({ selector: 'app-styled-component', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, template: ` <!-- ✅ Correct: Direct class and style bindings --> <div class="base-card" [class.active]="isActive()" [class.highlighted]="isHighlighted()" [class.loading]="isLoading()" [style.background-color]="backgroundColor()" [style.font-size.px]="fontSize()" [style.opacity]="opacity()" > <h2 [class.text-success]="isSuccess()">{{title()}}</h2> <p [class.text-muted]="isMuted()">{{content()}}</p> <!-- Dynamic class object --> <div [class]="dynamicClasses()"> Dynamic class binding </div> <!-- Dynamic style object --> <div [style]="dynamicStyles()"> Dynamic style binding </div> </div> ` }) export class StyledComponent { isActive = signal(false); isHighlighted = signal(true); isLoading = signal(false); isSuccess = signal(true); isMuted = signal(false); title = signal('Styled Component'); content = signal('This component uses modern class and style bindings'); // Computed signals for dynamic styling backgroundColor = computed(() => this.isActive() ? '#f8f9fa' : '#ffffff' ); fontSize = computed(() => this.isHighlighted() ? 18 : 16 ); opacity = computed(() => this.isLoading() ? 0.7 : 1 ); // Dynamic class object dynamicClasses = computed(() => ({ 'card-body': true, 'shadow-sm': this.isActive(), 'border-primary': this.isHighlighted(), 'text-center': this.isSuccess() })); // Dynamic style object dynamicStyles = computed(() => ({ 'padding': '1rem', 'margin-top': '0.5rem', 'border-radius': '0.25rem', 'border': this.isHighlighted() ? '2px solid #007bff' : '1px solid #dee2e6' })); } ``` **✅ Correct: Host Bindings in Component Decorator** ```typescript import { Component, input, signal, HostListener } from '@angular/core'; @Component({ selector: 'app-tooltip', standalone: true, template: ` <div class="tooltip-content"> {{content()}} </div> `, // ✅ Correct: Host bindings in decorator instead of @HostBinding host: { 'role': 'tooltip', '[attr.aria-hidden]': '!isVisible()', '[class.visible]': 'isVisible()', '[style.position]': "'absolute'", '[style.z-index]': '1000', '[style.opacity]': 'isVisible() ? 1 : 0', '[style.pointer-events]': 'isVisible() ? "auto" : "none"', '(mouseenter)': 'onMouseEnter()', '(mouseleave)': 'onMouseLeave()', '(keydown.escape)': 'onEscape()' } }) export class TooltipComponent { content = input.required<string>(); isVisible = signal(false); onMouseEnter() { this.isVisible.set(true); } onMouseLeave() { this.isVisible.set(false); } onEscape() { this.isVisible.set(false); } } // ✅ Correct: Directive with host bindings @Directive({ selector: '[appFocusTrap]', host: { '(keydown.tab)': 'onTabKeyDown($event)', '(keydown.shift.tab)': 'onShiftTabKeyDown($event)', '[attr.tabindex]': '0' } }) export class FocusTrapDirective { private elementRef = inject(ElementRef); onTabKeyDown(event: KeyboardEvent) { // Handle tab key navigation } onShiftTabKeyDown(event: KeyboardEvent) { // Handle shift+tab key navigation } } ``` **❌ Incorrect: Using Deprecated Decorators and ngClass/ngStyle** ```typescript // ❌ Avoid this pattern @Component({ selector: 'app-legacy-component', template: ` <!-- ❌ Incorrect: Using ngClass and ngStyle --> <div [ngClass]="{ 'active': isActive, 'highlighted': isHighlighted, 'loading': isLoading }" [ngStyle]="{ 'backgroundColor': backgroundColor, 'fontSize': fontSize + 'px', 'opacity': opacity }"> <h2>{{title}}</h2> <p>{{content}}</p> </div> ` }) export class LegacyComponent { // ❌ Incorrect: Using @Input and @Output decorators @Input() title: string = ''; @Input() content: string = ''; @Output() save = new EventEmitter<void>(); isActive = false; isHighlighted = true; isLoading = false; backgroundColor = '#ffffff'; fontSize = 16; opacity = 1; // ❌ Incorrect: Using @HostBinding and @HostListener @HostBinding('class.active') get activeClass() { return this.isActive; } @HostBinding('style.backgroundColor') get bgColor() { return this.backgroundColor; } @HostListener('mouseenter') onMouseEnter() { this.isActive = true; } @HostListener('mouseleave') onMouseLeave() { this.isActive = false; } } ``` ### Template Standards - Keep templates simple and avoid complex logic - Use native control flow for all conditional rendering and iteration - Use the async pipe to handle observables - Avoid complex expressions in templates #### Code Examples **✅ Correct: Clean Template with Native Control Flow** ```typescript import { Component, signal, inject, computed } from '@angular/core'; import { CommonModule } from '@angular/common'; import { DataService } from './data.service'; @Component({ selector: 'app-clean-template', standalone: true, imports: [CommonModule], template: ` <!-- ✅ Correct: Simple, readable template with native control flow --> <div class="container"> <header> <h1>{{pageTitle()}}</h1> <p>{{pageDescription()}}</p> </header> <!-- Loading and error states --> @if (isLoading()) { <div class="loading-spinner"> <span>Loading...</span> </div> } @else if (error()) { <div class="error-message"> <strong>Error:</strong> {{error()}} <button (click)="retry()">Retry</button> </div> } @else { <!-- Main content --> <main> <!-- User info section --> <section class="user-section"> <h2>User Information</h2> @if (currentUser(); as user) { <div class="user-card"> <h3>{{user.name}}</h3> <p>Email: {{user.email}}</p> <p>Role: {{user.role}}</p> <div [class.active]="user.isActive"> Status: {{user.isActive ? 'Active' : 'Inactive'}} </div> </div> } @else { <p>No user selected</p> } </section> <!-- Items list --> <section class="items-section"> <h2>Items</h2> @if (filteredItems().length > 0) { <div class="items-grid"> @for (item of filteredItems(); track item.id) { <div class="item-card" [class.featured]="item.isFeatured"> <h4>{{item.name}}</h4> <p>{{item.description}}</p> <p>Price: ${{item.price | number:'1.2-2'}}</p> <button (click)="selectItem(item)"> {{item.isSelected ? 'Selected' : 'Select'}} </button> </div> } @empty { <p class="no-items">No items found</p> } </div> } @else { <p class="no-items">No items available</p> } </section> <!-- Status indicators --> <section class="status-section"> <h2>System Status</h2> @switch (systemStatus()) { @case ('online') { <div class="status-online"> <span class="status-indicator online"></span> System is online </div> } @case ('maintenance') { <div class="status-maintenance"> <span class="status-indicator maintenance"></span> System under maintenance </div> } @case ('offline') { <div class="status-offline"> <span class="status-indicator offline"></span> System is offline </div> } @default { <div class="status-unknown"> <span class="status-indicator unknown"></span> Status unknown </div> } } </section> </main> } </div> ` }) export class CleanTemplateComponent { private dataService = inject(DataService); // Signal-based state pageTitle = signal('Dashboard'); pageDescription = signal('Welcome to your dashboard'); isLoading = signal(false); error = signal<string | null>(null); currentUser = signal<User | null>(null); systemStatus = signal<'online' | 'maintenance' | 'offline'>('online'); // Observable data with async pipe in template items$ = this.dataService.getItems(); selectedCategory = signal<string>('all'); // Computed signals for derived state filteredItems = computed(() => { const items = this.items$ | async; const category = this.selectedCategory(); if (!items) return []; if (category === 'all') return items; return items.filter(item => item.category === category); }); constructor() { this.loadInitialData(); } private loadInitialData() { this.isLoading.set(true); this.error.set(null); this.dataService.getCurrentUser().subscribe({ next: (user) => { this.currentUser.set(user); this.isLoading.set(false); }, error: (err) => { this.error.set(err.message); this.isLoading.set(false); } }); } retry() { this.loadInitialData(); } selectItem(item: Item) { this.dataService.selectItem(item.id).subscribe(); } } ``` **✅ Correct: Async Pipe with Complex Data** ```typescript import { Component, inject, signal, computed } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Observable, combineLatest } from 'rxjs'; import { map, switchMap } from 'rxjs/operators'; import { ApiService } from './api.service'; @Component({ selector: 'app-async-example', standalone: true, imports: [CommonModule], template: ` <!-- ✅ Correct: Using async pipe for complex observable chains --> <div class="dashboard"> <!-- Combined data streams --> @if (combinedData$ | async; as data) { <div class="stats"> <div class="stat-card"> <h3>Total Users</h3> <p>{{data.userCount}}</p> </div> <div class="stat-card"> <h3>Active Sessions</h3> <p>{{data.activeSessions}}</p> </div> <div class="stat-card"> <h3>Revenue</h3> <p>${{data.revenue | number:'1.2-2'}}</p> </div> </div> <!-- Nested async data --> <div class="recent-activity"> <h3>Recent Activity</h3> @if (data.recentActivity$ | async; as activities) { <ul> @for (activity of activities; track activity.id) { <li [class]="activity.type"> <strong>{{activity.user}}:</strong> {{activity.action}} <small>{{activity.timestamp | date:'short'}}</small> </li> } @empty { <li>No recent activity</li> } </ul> } </div> } <!-- Real-time updates --> <div class="real-time-updates"> <h3>Live Updates</h3> @if (liveUpdates$ | async; as updates) { <div class="update-feed"> @for (update of updates; track update.id) { <div class="update-item"> <span class="update-time">{{update.timestamp | date:'shortTime'}}</span> <span class="update-message">{{update.message}}</span> </div> } </div> } </div> </div> ` }) export class AsyncExampleComponent { private apiService = inject(ApiService); // Complex observable chain combinedData$ = combineLatest([ this.apiService.getUserCount(), this.apiService.getActiveSessions(), this.apiService.getRevenue() ]).pipe( map(([userCount, activeSessions, revenue]) => ({ userCount, activeSessions, revenue, recentActivity$: this.apiService.getRecentActivity() })) ); // Real-time updates liveUpdates$ = this.apiService.getLiveUpdates(); // Signal-based filters refreshInterval = signal<number>(5000); maxUpdates = signal<number>(10); // Computed observable based on signals filteredUpdates$ = computed(() => this.liveUpdates$.pipe( map(updates => updates.slice(0, this.maxUpdates())) ) )(); } ``` **❌ Incorrect: Complex Template Logic** ```typescript // ❌ Avoid this pattern @Component({ selector: 'app-complex-template', template: ` <!-- ❌ Incorrect: Complex logic in template --> <div> <!-- Complex conditional logic --> <div *ngIf="users && users.length > 0 && !loading && !error; else loadingTemplate"> <!-- Nested loops with complex expressions --> <div *ngFor="let user of users; let i = index; trackBy: trackUser"> <div *ngIf="user.isActive && user.roles.includes('admin') || user.permissions.some(p => p.level === 'full')" [class.admin]="user.roles.includes('admin')" [class.premium]="user.subscription?.type === 'premium' || user.subscription?.expires > new Date()" [style.background-color]="user.status === 'active' ? '#e8f5e8' : user.status === 'inactive' ? '#ffe8e8' : '#f0f0f0'" > <h3>{{user.firstName + ' ' + user.lastName}}</h3> <p>Email: {{user.email}}</p> <p>Age: {{new Date().getFullYear() - new Date(user.birthDate).getFullYear()}}</p> <!-- Complex nested conditions --> <div *ngIf="user.projects && user.projects.length > 0"> <h4>Projects ({{user.projects.length}})</h4> <div *ngFor="let project of user.projects; let j = index"> <div [class.completed]="project.status === 'completed'" [class.in-progress]="project.status === 'in-progress'" [class.delayed]="project.deadline < new Date() && project.status !== 'completed'" > {{project.name}} - {{project.status}} <small>Due: {{project.deadline | date:'shortDate'}}</small> </div> </div> </div> <!-- Complex method calls in template --> <button [disabled]="!canEditUser(user) || user.isLocked || user.roles.includes('super-admin')" (click)="editUser(user, i)" > {{user.isEditable ? 'Edit' : 'View'}} </button> </div> </div> </div> <!-- Complex ng-template --> <ng-template #loadingTemplate> <div *ngIf="loading"> Loading... </div> <div *ngIf="error"> Error: {{error.message}} </div> <div *ngIf="!loading && !error && (!users || users.length === 0)"> No users found </div> </ng-template> </div> ` }) export class ComplexTemplateComponent { users: any[] = []; loading = false; error: any = null; // ❌ Incorrect: Complex business logic in component canEditUser(user: any): boolean { const userRole = user.roles?.[0] || 'user'; const userPermissions = user.permissions || []; const hasEditPermission = userPermissions.some(p => p.action === 'edit' && p.resource === 'user'); const isNotSuperAdmin = !user.roles?.includes('super-admin'); const isNotLocked = !user.isLocked; const accountActive = user.status === 'active'; const subscriptionValid = user.subscription?.expires > new Date(); return hasEditPermission && isNotSuperAdmin && isNotLocked && accountActive && subscriptionValid; } trackUser(index: number, user: any): number { return user.id; } editUser(user: any, index: number) { // Complex edit logic console.log(`Editing user ${user.name} at index ${index}`); } } ``` ### Service Standards - Use the `inject()` function instead of constructor injection - Design services around a single responsibility - Use the `providedIn: 'root'` option for singleton services - Keep service methods focused and cohesive #### Code Examples **✅ Correct: Modern Service with inject() Function** ```typescript import { Injectable, inject, DestroyRef } from '@angular/core'; import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Observable, throwError, BehaviorSubject } from 'rxjs'; import { catchError, tap, switchMap, takeUntilDestroyed } from 'rxjs/operators'; @Injectable({ providedIn: 'root' }) export class UserService { // ✅ Correct: Using inject() function instead of constructor injection private http = inject(HttpClient); private destroyRef = inject(DestroyRef); // Signal-based state management private currentUserSubject = new BehaviorSubject<User | null>(null); currentUser$ = this.currentUserSubject.asObservable(); private loadingSubject = new BehaviorSubject<boolean>(false); loading$ = this.loadingSubject.asObservable(); // API configuration private apiUrl = 'https://api.example.com/users'; private httpOptions = { headers: new HttpHeaders({ 'Content-Type': 'application/json' }) }; // ✅ Correct: Single responsibility - only user-related operations getUsers(params?: UserSearchParams): Observable<User[]> { this.loadingSubject.next(true); return this.http.get<User[]>(this.apiUrl, { params: params ? this.formatParams(params) : undefined }).pipe( tap(users => { console.log(`Fetched ${users.length} users`); this.loadingSubject.next(false); }), catchError(error => { this.loadingSubject.next(false); return throwError(() => new Error(`Failed to fetch users: ${error.message}`)); }), takeUntilDestroyed(this.destroyRef) ); } getUserById(id: string): Observable<User> { return this.http.get<User>(`${this.apiUrl}/${id}`).pipe( catchError(error => throwError(() => new Error(`User not found: ${error.message}`))) ); } createUser(userData: CreateUserRequest): Observable<User> { return this.http.post<User>(this.apiUrl, userData, this.httpOptions).pipe( tap(user => { console.log(`Created user: ${user.id}`); this.refreshUserList(); }), catchError(error => throwError(() => new Error(`Failed to create user: ${error.message}`))) ); } updateUser(id: string, userData: UpdateUserRequest): Observable<User> { return this.http.put<User>(`${this.apiUrl}/${id}`, userData, this.httpOptions).pipe( tap(user => { console.log(`Updated user: ${user.id}`); // Update current user if it's the same user this.currentUser$.pipe( takeUntilDestroyed(this.destroyRef) ).subscribe(currentUser => { if (currentUser && currentUser.id === id) { this.currentUserSubject.next({ ...currentUser, ...userData }); } }); }), catchError(error => throwError(() => new Error(`Failed to update user: ${error.message}`))) ); } deleteUser(id: string): Observable<void> { return this.http.delete<void>(`${this.apiUrl}/${id}`).pipe( tap(() => { console.log(`Deleted user: ${id}`); this.refreshUserList(); }), catchError(error => throwError(() => new Error(`Failed to delete user: ${error.message}`))) ); } setCurrentUser(user: User): void { this.currentUserSubject.next(user); } getCurrentUser(): User | null { return this.currentUserSubject.value; } clearCurrentUser(): void { this.currentUserSubject.next(null); } // ✅ Correct: Private helper method private formatParams(params: UserSearchParams): Record<string, string> { return { search: params.search || '', role: params.role || '', status: params.status || '', limit: params.limit?.toString() || '10', offset: params.offset?.toString() || '0' }; } // ✅ Correct: Private helper method private refreshUserList(): void { this.getUsers().subscribe(); } } // ✅ Correct: Separate service for authentication @Injectable({ providedIn: 'root' }) export class AuthService { private http = inject(HttpClient); private destroyRef = inject(DestroyRef); private authStateSubject = new BehaviorSubject<AuthState>({ isAuthenticated: false, token: null, user: null }); authState$ = this.authStateSubject.asObservable(); login(credentials: LoginCredentials): Observable<AuthResponse> { return this.http.post<AuthResponse>('https://api.example.com/auth/login', credentials).pipe( tap(response => { this.authStateSubject.next({ isAuthenticated: true, token: response.token, user: response.user }); localStorage.setItem('auth_token', response.token); }), catchError(error => throwError(() => new Error('Login failed'))) ); } logout(): void { localStorage.removeItem('auth_token'); this.authStateSubject.next({ isAuthenticated: false, token: null, user: null }); } refreshToken(): Observable<AuthResponse> { const currentToken = localStorage.getItem('auth_token'); if (!currentToken) { return throwError(() => new Error('No token available')); } return this.http.post<AuthResponse>('https://api.example.com/auth/refresh', { token: currentToken }).pipe( tap(response => { this.authStateSubject.next({ isAuthenticated: true, token: response.token, user: response.user }); localStorage.setItem('auth_token', response.token); }) ); } } // ✅ Correct: Service for API configuration @Injectable({ providedIn: 'root' }) export class ApiConfigService { private http = inject(HttpClient); getApiConfig(): Observable<ApiConfig> { return this.http.get<ApiConfig>('https://api.example.com/config'); } getFeatureFlags(): Observable<FeatureFlags> { return this.http.get<FeatureFlags>('https://api.example.com/features'); } } ``` **❌ Incorrect: Constructor Injection and Multi-Responsibility Service** ```typescript // ❌ Avoid this pattern @Injectable() export class LegacyService { // ❌ Incorrect: Constructor injection instead of inject() constructor( private http: HttpClient, private authService: AuthService, private configService: ApiConfigService, private logger: LoggerService, private cache: CacheService, private analytics: AnalyticsService ) {} // ❌ Incorrect: Multiple responsibilities in one service getUsers(): Observable<User[]> { return this.http.get<User[]>('/api/users'); } authenticateUser(credentials: LoginCredentials): Observable<AuthResponse> { return this.http.post<AuthResponse>('/api/auth/login', credentials); } logError(error: Error): void { this.logger.error(error); } cacheData<T>(key: string, data: T): void { this.cache.set(key, data); } trackEvent(event: string, properties: any): void { this.analytics.track(event, properties); } getConfig(): Observable<Config> { return this.configService.getConfig(); } // ❌ Incorrect: Methods doing too many things complexOperation(data: any): Observable<any> { this.logger.info('Starting complex operation'); return this.http.post('/api/operation', data).pipe( tap(result => { this.cacheData('operation_result', result); this.trackEvent('operation_completed', { success: true }); this.logger.info('Operation completed successfully'); }), catchError(error => { this.logError(error); this.trackEvent('operation_failed', { error: error.message }); return throwError(() => error); }) ); } } ``` ## Architecture Constraints ### Dependency Injection - Constructor injection is discouraged in favor of `inject()` function - All services must specify their providedIn property - Tree-shaking must be considered for service injection #### Code Examples **✅ Correct: Modern Dependency Injection with inject()** ```typescript import { Component, inject, Injectable, DestroyRef, signal, computed } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Router } from '@angular/router'; import { AuthService } from './auth.service'; import { takeUntilDestroyed } from 'rxjs/operators'; // ✅ Correct: Component using inject() function @Component({ selector: 'app-user-profile', standalone: true, template: ` <div class="profile"> @if (user(); as user) { <h2>{{user.name}}</h2> <p>{{user.email}}</p> <button (click)="logout()">Logout</button> } @else { <p>Please log in</p> } </div> ` }) export class UserProfileComponent { // ✅ Correct: Using inject() instead of constructor private authService = inject(AuthService); private router = inject(Router); private destroyRef = inject(DestroyRef); // Signal-based state user = signal<User | null>(null); constructor() { this.loadUserProfile(); } private loadUserProfile() { this.authService.getCurrentUser().pipe( takeUntilDestroyed(this.destroyRef) ).subscribe({ next: (user) => this.user.set(user), error: () => this.router.navigate(['/login']) }); } logout() { this.authService.logout(); this.router.navigate(['/login']); } } // ✅ Correct: Service with proper provi