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
Markdown
# 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