|
| 1 | +--- |
| 2 | +description: APPLY Angular best practices WHEN developing a scalable web application architecture in Angular |
| 3 | +globs: src/*/*.ts, src/*/*.html,src/*/*.scss |
| 4 | +alwaysApply: false |
| 5 | +--- |
| 6 | + |
| 7 | +You are an expert in TypeScript, Angular, and scalable web application development. You write maintainable, performant, and accessible code following Angular and TypeScript best practices. |
| 8 | + |
| 9 | +## Context |
| 10 | +- Develop a standalone Angular application using TypeScript. |
| 11 | +- Ensure clarity, readability, optimal performance, and enterprise-ready architecture. |
| 12 | +- Follow best practices from Angular, Angular Material, TypeScript, and SASS official documentation. |
| 13 | +- Implement Clean Architecture principles and modern Angular patterns. |
| 14 | + |
| 15 | +## Naming Conventions |
| 16 | +- **File Names**: Use kebab-case with descriptive suffixes |
| 17 | + - `*.component.ts` for Components |
| 18 | + - `*.service.ts` for Services |
| 19 | + - `*.facade.ts` for Facade services |
| 20 | + - `*.repository.ts` for Repository services |
| 21 | + - `*.directive.ts` for Directives |
| 22 | + - `*.pipe.ts` for Pipes |
| 23 | + - `*.spec.ts` for Tests |
| 24 | + - `*.model.ts` for Interfaces/Types |
| 25 | +- **Variable Naming**: |
| 26 | + - camelCase for variables, functions, properties |
| 27 | + - PascalCase for classes, interfaces, types, enums |
| 28 | + - UPPER_SNAKE_CASE for constants |
| 29 | +- **Component Selectors**: Use consistent prefix (e.g., `app-`, `shared-`, `feature-`) |
| 30 | + |
| 31 | + |
| 32 | +### Code Structure Pattern |
| 33 | +```typescript |
| 34 | +// 1. Imports |
| 35 | +import { Component, inject, signal } from '@angular/core'; |
| 36 | +import { MatButtonModule } from '@angular/material/button'; |
| 37 | + |
| 38 | +// 2. Interfaces/Types |
| 39 | +interface UserData { |
| 40 | + id: string; |
| 41 | + name: string; |
| 42 | +} |
| 43 | + |
| 44 | +// 3. Component Definition |
| 45 | +@Component({ |
| 46 | + selector: 'app-user-profile', |
| 47 | + standalone: true, |
| 48 | + imports: [MatButtonModule], |
| 49 | + templateUrl: './user-profile.component.html', |
| 50 | + styleUrls: ['./user-profile.component.scss'], |
| 51 | + changeDetection: ChangeDetectionStrategy.OnPush |
| 52 | +}) |
| 53 | +export class UserProfileComponent { |
| 54 | + // 4. Injected Dependencies |
| 55 | + private readonly userService = inject(UserService); |
| 56 | + |
| 57 | + // 5. Signals and State |
| 58 | + protected readonly user = signal<UserData | null>(null); |
| 59 | + protected readonly isLoading = signal<boolean>(false); |
| 60 | + |
| 61 | + // 6. Computed Values |
| 62 | + protected readonly displayName = computed(() => |
| 63 | + this.user()?.name ?? 'Anonymous User' |
| 64 | + ); |
| 65 | + |
| 66 | + // 7. Lifecycle Hooks |
| 67 | + ngOnInit(): void { |
| 68 | + this.loadUser(); |
| 69 | + } |
| 70 | + |
| 71 | + // 8. Public Methods |
| 72 | + public refreshUser(): void { |
| 73 | + this.loadUser(); |
| 74 | + } |
| 75 | + |
| 76 | + // 9. Protected Methods (Template accessible) |
| 77 | + protected onSaveClick(): void { |
| 78 | + this.saveUser(); |
| 79 | + } |
| 80 | + |
| 81 | + // 10. Private Methods |
| 82 | + private loadUser(): void { |
| 83 | + // Implementation |
| 84 | + } |
| 85 | +} |
| 86 | +``` |
| 87 | + |
| 88 | +## Angular-Specific Guidelines |
| 89 | + |
| 90 | +### Best Practices |
| 91 | +- Always use standalone components over NgModules |
| 92 | +- Don't use explicit standalone: true (it is implied by default) |
| 93 | +- Use signals for state management |
| 94 | +- Implement lazy loading for feature routes |
| 95 | +- Use NgOptimizedImage for all static images. |
| 96 | +- Control Flow: Use `@if`, `@for`, `@switch` instead of structural directives |
| 97 | +- Defer Blocks: Implement lazy loading for non-critical content |
| 98 | +- View Transitions: Use Angular's view transition API |
| 99 | +- Signal-based Routing: Leverage signal inputs in routes |
| 100 | + |
| 101 | +```html |
| 102 | +<!-- Modern Control Flow --> |
| 103 | +@if (user(); as currentUser) { |
| 104 | + <div class="user-profile"> |
| 105 | + <h2>{{ currentUser.name }}</h2> |
| 106 | + @for (role of currentUser.roles; track role.id) { |
| 107 | + <span class="role-badge">{{ role.name }}</span> |
| 108 | + } |
| 109 | + </div> |
| 110 | +} @else { |
| 111 | + <div class="login-prompt">Please log in</div> |
| 112 | +} |
| 113 | + |
| 114 | +<!-- Defer Blocks for Performance --> |
| 115 | +@defer (when shouldLoadChart) { |
| 116 | + <app-analytics-chart [data]="chartData()" /> |
| 117 | +} @placeholder { |
| 118 | + <div class="chart-placeholder">Loading chart...</div> |
| 119 | +} @loading (minimum 500ms) { |
| 120 | + <mat-spinner></mat-spinner> |
| 121 | +} @error { |
| 122 | + <div class="error-message">Failed to load chart</div> |
| 123 | +} |
| 124 | +``` |
| 125 | + |
| 126 | +### Components |
| 127 | +- Keep components small and focused on a single responsibility |
| 128 | +- Use input() and output() functions instead of decorators |
| 129 | +- Use computed() for derived state |
| 130 | +- Set `changeDetection: ChangeDetectionStrategy.OnPush` in `@Component` decorator |
| 131 | +- Prefer inline templates for small components |
| 132 | +- Prefer Reactive forms instead of Template-driven ones |
| 133 | +- Do NOT use "ngClass" (NgClass), use "class" bindings instead |
| 134 | +- DO NOT use "ngStyle" (NgStyle), use "style" bindings instead |
| 135 | +- Always use standalone components |
| 136 | +- Separate files: Keep HTML, TypeScript, and SCSS in separate files |
| 137 | +- ChangeDetectionStrategy.OnPush: Mandatory for all components |
| 138 | +- Consistent selector prefix: Use app-wide naming convention |
| 139 | +- Input validation: Use proper typing and validation for @Input properties |
| 140 | +- Output naming: Use consistent event naming (e.g., `userSelected`, `formSubmitted`) |
| 141 | + |
| 142 | +```typescript |
| 143 | +@Component({ |
| 144 | + selector: 'app-user-card', |
| 145 | + imports: [MatCardModule, MatButtonModule], |
| 146 | + templateUrl: './user-card.component.html', |
| 147 | + styleUrls: ['./user-card.component.scss'], |
| 148 | + changeDetection: ChangeDetectionStrategy.OnPush |
| 149 | +}) |
| 150 | +export class UserCardComponent { |
| 151 | + @Input({ required: true }) user!: User; |
| 152 | + @Output() userSelected = new EventEmitter<User>(); |
| 153 | + |
| 154 | + protected onSelectUser(): void { |
| 155 | + this.userSelected.emit(this.user); |
| 156 | + } |
| 157 | +} |
| 158 | +``` |
| 159 | + |
| 160 | +### Services and Dependency Injection |
| 161 | +- Use inject() function: Preferred over constructor injection |
| 162 | +- Proper visibility: Mark methods as public, protected, or private appropriately |
| 163 | +- Single Responsibility: One service per domain concern |
| 164 | +- Facade Pattern: Use facades for complex feature interactions |
| 165 | +- Design services around a single responsibility |
| 166 | +- Use the providedIn: 'root' option for singleton services |
| 167 | +- Use the inject() function instead of constructor injection |
| 168 | + |
| 169 | +```typescript |
| 170 | +@Injectable({ providedIn: 'root' }) |
| 171 | +export class UserService { |
| 172 | + private readonly http = inject(HttpClient); |
| 173 | + private readonly apiUrl = environment.apiUrl; |
| 174 | + |
| 175 | + public getUsers(): Observable<User[]> { |
| 176 | + return this.http.get<User[]>(`${this.apiUrl}/users`); |
| 177 | + } |
| 178 | + |
| 179 | + public getUserById(id: string): Observable<User> { |
| 180 | + return this.http.get<User>(`${this.apiUrl}/users/${id}`); |
| 181 | + } |
| 182 | +} |
| 183 | + |
| 184 | +// Facade Pattern Example |
| 185 | +@Injectable({ providedIn: 'root' }) |
| 186 | +export class UserFacade { |
| 187 | + private readonly userService = inject(UserService); |
| 188 | + private readonly notificationService = inject(NotificationService); |
| 189 | + |
| 190 | + public readonly users = signal<User[]>([]); |
| 191 | + public readonly selectedUser = signal<User | null>(null); |
| 192 | + |
| 193 | + public async loadUsers(): Promise<void> { |
| 194 | + try { |
| 195 | + const users = await firstValueFrom(this.userService.getUsers()); |
| 196 | + this.users.set(users); |
| 197 | + } catch (error) { |
| 198 | + this.notificationService.showError('Failed to load users'); |
| 199 | + } |
| 200 | + } |
| 201 | +} |
| 202 | +``` |
| 203 | + |
| 204 | +### State Management with Signals |
| 205 | +- Prefer signals: Use Angular signals for reactive state management |
| 206 | +- Signal composition: Leverage computed signals for derived state |
| 207 | +- Effect usage: Use effects sparingly, prefer computed signals |
| 208 | +- Resource API: Use for async data loading patterns |
| 209 | +- Use signals for local component state |
| 210 | +- Use computed() for derived state |
| 211 | +- Keep state transformations pure and predictable |
| 212 | + |
| 213 | + |
| 214 | +```typescript |
| 215 | +// Signal-based State Management |
| 216 | +export class ProductStore { |
| 217 | + // Base signals |
| 218 | + private readonly _products = signal<Product[]>([]); |
| 219 | + private readonly _selectedCategory = signal<string>('all'); |
| 220 | + private readonly _isLoading = signal<boolean>(false); |
| 221 | + |
| 222 | + // Read-only public signals |
| 223 | + public readonly products = this._products.asReadonly(); |
| 224 | + public readonly selectedCategory = this._selectedCategory.asReadonly(); |
| 225 | + public readonly isLoading = this._isLoading.asReadonly(); |
| 226 | + |
| 227 | + // Computed signals |
| 228 | + public readonly filteredProducts = computed(() => { |
| 229 | + const products = this._products(); |
| 230 | + const category = this._selectedCategory(); |
| 231 | + return category === 'all' |
| 232 | + ? products |
| 233 | + : products.filter(p => p.category === category); |
| 234 | + }); |
| 235 | + |
| 236 | + public readonly productCount = computed(() => this.filteredProducts().length); |
| 237 | + |
| 238 | + // Resource API for async data |
| 239 | + public readonly productsResource = resource({ |
| 240 | + request: () => this.selectedCategory(), |
| 241 | + loader: ({ request: category }) => this.loadProductsByCategory(category) |
| 242 | + }); |
| 243 | + |
| 244 | + // State mutations |
| 245 | + public setProducts(products: Product[]): void { |
| 246 | + this._products.set(products); |
| 247 | + } |
| 248 | + |
| 249 | + public selectCategory(category: string): void { |
| 250 | + this._selectedCategory.set(category); |
| 251 | + } |
| 252 | +} |
| 253 | +``` |
| 254 | + |
| 255 | +### Reactive Programming Best Practices |
| 256 | +- Async pipe: Always use async pipe in templates for Observables |
| 257 | +- Signal interop: Use `toSignal()` and `toObservable()` for RxJS integration |
| 258 | +- Subscription management: Use `takeUntilDestroyed()` for automatic cleanup |
| 259 | +- Error handling: Always handle Observable errors appropriately |
| 260 | + |
| 261 | +```typescript |
| 262 | +export class DataComponent { |
| 263 | + private readonly destroyRef = inject(DestroyRef); |
| 264 | + private readonly dataService = inject(DataService); |
| 265 | + |
| 266 | + // Convert Observable to Signal |
| 267 | + protected readonly data = toSignal( |
| 268 | + this.dataService.getData().pipe( |
| 269 | + catchError(error => { |
| 270 | + console.error('Data loading failed:', error); |
| 271 | + return of([]); |
| 272 | + }) |
| 273 | + ), |
| 274 | + { initialValue: [] } |
| 275 | + ); |
| 276 | + |
| 277 | + // Manual subscription with cleanup |
| 278 | + private subscribeToUpdates(): void { |
| 279 | + this.dataService.getUpdates() |
| 280 | + .pipe(takeUntilDestroyed(this.destroyRef)) |
| 281 | + .subscribe(update => this.handleUpdate(update)); |
| 282 | + } |
| 283 | +} |
| 284 | +``` |
| 285 | + |
| 286 | +### Templates |
| 287 | +- Keep templates simple and avoid complex logic |
| 288 | +- Use native control flow (@if, @for, @switch) instead of *ngIf, *ngFor, *ngSwitch |
| 289 | +- Use the async pipe to handle observables |
| 290 | + |
| 291 | +## TypeScript Best Practices |
| 292 | +- Use strict type checking |
| 293 | +- Prefer type inference when the type is obvious |
| 294 | +- Avoid the `any` type; use `unknown` when type is uncertain |
| 295 | + |
| 296 | +## References |
| 297 | + |
| 298 | +### Core Angular |
| 299 | +- [Angular Developer Guide](mdc:https:/angular.dev) |
| 300 | +- [Angular API Reference](mdc:https:/angular.dev/api) |
| 301 | +- [Angular CLI Documentation](mdc:https:/angular.dev/cli) |
| 302 | +- [Angular Update Guide](mdc:https:/angular.dev/update-guide) |
| 303 | + |
| 304 | +### Components & Templates |
| 305 | +- [Component Overview](mdc:https:/angular.dev/guide/components) |
| 306 | +- [Template Syntax](mdc:https:/angular.dev/guide/templates) |
| 307 | +- [Control Flow](mdc:https:/angular.dev/guide/templates/control-flow) |
| 308 | +- [Defer Blocks](mdc:https:/angular.dev/guide/templates/defer) |
| 309 | + |
| 310 | +### Signals & State Management |
| 311 | +- [Signals Overview](mdc:https:/angular.dev/guide/signals) |
| 312 | +- [Signal Inputs](mdc:https:/angular.dev/guide/signals#signal-inputs) |
| 313 | +- [Computed Signals](mdc:https:/angular.dev/guide/signals#computed-signals) |
| 314 | +- [Signal Effects](mdc:https:/angular.dev/guide/signals#effects) |
| 315 | +- [Resource API](mdc:https:/angular.dev/guide/signals/resource) |
| 316 | +- [RxJS Interop](mdc:https:/angular.dev/guide/rxjs-interop) |
| 317 | + |
| 318 | +### Dependency Injection |
| 319 | +- [DI Overview](mdc:https:/angular.dev/guide/di) |
| 320 | +- [Injectable Services](mdc:https:/angular.dev/guide/di/creating-injectable-service) |
| 321 | +- [Injection Context](mdc:https:/angular.dev/guide/di/dependency-injection-context) |
| 322 | +- [Hierarchical Injectors](mdc:https:/angular.dev/guide/di/hierarchical-dependency-injection) |
| 323 | + |
| 324 | +### HTTP & Data Loading |
| 325 | +- [HttpClient Guide](mdc:https:/angular.dev/guide/http) |
| 326 | +- [HTTP Interceptors](mdc:https:/angular.dev/guide/http/interceptors) |
| 327 | +- [HTTP Testing](mdc:https:/angular.dev/guide/http/testing) |
| 328 | + |
| 329 | +### Forms |
| 330 | +- [Forms Overview](mdc:https:/angular.dev/guide/forms) |
| 331 | +- [Reactive Forms](mdc:https:/angular.dev/guide/forms/reactive-forms) |
| 332 | +- [Form Validation](mdc:https:/angular.dev/guide/forms/form-validation) |
| 333 | +- [Dynamic Forms](mdc:https:/angular.dev/guide/forms/dynamic-forms) |
| 334 | + |
| 335 | +### Routing |
| 336 | +- [Router Overview](mdc:https:/angular.dev/guide/routing) |
| 337 | +- [Route Guards](mdc:https:/angular.dev/guide/routing/guards) |
| 338 | +- [Lazy Loading](mdc:https:/angular.dev/guide/routing/lazy-loading) |
| 339 | + |
| 340 | +### Testing |
| 341 | +- [Testing Overview](mdc:https:/angular.dev/guide/testing) |
| 342 | +- [Component Testing](mdc:https:/angular.dev/guide/testing/components-basics) |
| 343 | +- [Service Testing](mdc:https:/angular.dev/guide/testing/services) |
| 344 | +- [E2E Testing](mdc:https:/angular.dev/guide/testing/e2e) |
| 345 | + |
| 346 | +### Performance |
| 347 | +- [Performance Guide](mdc:https:/angular.dev/guide/performance) |
| 348 | +- [Bundle Optimization](mdc:https:/angular.dev/guide/performance/bundle-optimization) |
| 349 | +- [Image Optimization](mdc:https:/angular.dev/guide/image-optimization) |
| 350 | + |
| 351 | +### Accessibility |
| 352 | +- [A11y Overview](mdc:https:/angular.dev/guide/accessibility) |
| 353 | +- [Angular CDK A11y](mdc:https:/material.angular.io/cdk/a11y/overview) |
| 354 | + |
| 355 | +### Angular Material |
| 356 | +- [Material Design](mdc:https:/material.angular.io) |
| 357 | +- [Material Components](mdc:https:/material.angular.io/components) |
| 358 | +- [Material Theming](mdc:https:/material.angular.io/guide/theming) |
| 359 | +- [Material CDK](mdc:https:/material.angular.io/cdk) |
| 360 | + |
| 361 | +### Best Practices |
| 362 | +- [Style Guide](mdc:https:/angular.dev/style-guide) |
| 363 | +- [Security Guide](mdc:https:/angular.dev/guide/security) |
| 364 | +- [Performance Best Practices](mdc:https:/angular.dev/guide/performance) |
| 365 | +- [Accessibility Best Practices](mdc:https:/angular.dev/guide/accessibility) |
0 commit comments