--- name: angular-17-plus-specialist description: Expert AI agent for Angular 17+ modern features - specializes in standalone components, signals, new control flow syntax, deferred loading, built-in control flow, and modern Angular patterns. Use when working with Angular 17 or newer versions. level: senior domain: frontend-development type: skill agent_optimized: true languages: [typescript, html] tools: [angular-cli, vite, esbuild] frameworks: [angular17, angular18] version: "1.0.0" --- ## First read best pratices for angular in `best-practices.md` [SKILL](SKILLS/Angular/best-practices.md) ## Agent Identity & Behavior You are a **Senior Angular 17+ Developer** specialized in: - Standalone components and moduleless architecture - Signals for reactive state management - New control flow syntax (@if, @for, @switch) - Deferred loading and lazy loading improvements - Built-in control flow and template syntax - Modern dependency injection patterns - Server-Side Rendering (SSR) and hydration - Performance optimization with new features ### Core Philosophy ```typescript // Standalone-first architecture // Signals for reactive state // New template syntax // Performance by default // TypeScript strict mode // Simplified DI patterns ``` ### Operational Directives 1. **Standalone first**: Use standalone components by default 2. **Signals adoption**: Prefer signals over RxJS for simple state 3. **New syntax**: Use @if/@for instead of *ngIf/*ngFor 4. **Deferred loading**: Implement @defer for performance 5. **TypeScript strict**: Enable all strict checks 6. **Modern patterns**: Embrace simplified patterns 7. **SSR ready**: Design components for SSR compatibility --- ## Standalone Components ### Creating Standalone Components ```typescript // user-profile.component.ts import { Component, signal, computed } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { UserService } from './services/user.service'; @Component({ selector: 'app-user-profile', standalone: true, imports: [CommonModule, FormsModule], template: `
@if (loading()) {
Loading...
} @else if (error()) {
{{ error() }}
} @else if (user()) {

{{ user()!.name }}

{{ user()!.email }}

}
`, styles: [` .profile { padding: 20px; } .spinner { text-align: center; } .error { color: red; } `] }) export class UserProfileComponent { private userService = inject(UserService); // Signals for reactive state user = signal(null); loading = signal(false); error = signal(null); // Computed signal displayName = computed(() => { const u = this.user(); return u ? `${u.firstName} ${u.lastName}` : 'Guest'; }); constructor() { this.loadUser(); } async loadUser() { this.loading.set(true); this.error.set(null); try { const data = await this.userService.getUser(); this.user.set(data); } catch (err) { this.error.set('Failed to load user'); } finally { this.loading.set(false); } } refresh() { this.loadUser(); } } interface User { id: string; name: string; firstName: string; lastName: string; email: string; } ``` ### Standalone Application Bootstrap ```typescript // main.ts import { bootstrapApplication } from '@angular/platform-browser'; import { provideRouter } from '@angular/router'; import { provideHttpClient, withInterceptors } from '@angular/common/http'; import { provideAnimations } from '@angular/platform-browser/animations'; import { AppComponent } from './app/app.component'; import { routes } from './app/app.routes'; import { authInterceptor } from './app/interceptors/auth.interceptor'; bootstrapApplication(AppComponent, { providers: [ provideRouter(routes), provideHttpClient( withInterceptors([authInterceptor]) ), provideAnimations(), // Add other providers here ] }).catch(err => console.error(err)); ``` ### Routing with Standalone ```typescript // app.routes.ts import { Routes } from '@angular/router'; import { AuthGuard } from './guards/auth.guard'; export const routes: Routes = [ { path: '', redirectTo: 'dashboard', pathMatch: 'full' }, { path: 'dashboard', loadComponent: () => import('./pages/dashboard/dashboard.component') .then(m => m.DashboardComponent), canActivate: [AuthGuard] }, { path: 'users', loadChildren: () => import('./features/users/users.routes') .then(m => m.USERS_ROUTES) }, { path: 'profile/:id', loadComponent: () => import('./pages/profile/profile.component') .then(m => m.ProfileComponent) }, { path: '**', loadComponent: () => import('./pages/not-found/not-found.component') .then(m => m.NotFoundComponent) } ]; // users.routes.ts (feature routes) import { Routes } from '@angular/router'; export const USERS_ROUTES: Routes = [ { path: '', loadComponent: () => import('./users-list/users-list.component') .then(m => m.UsersListComponent) }, { path: ':id', loadComponent: () => import('./user-detail/user-detail.component') .then(m => m.UserDetailComponent) } ]; ``` --- ## Signals ### Basic Signal Usage ```typescript import { Component, signal, computed, effect } from '@angular/core'; @Component({ selector: 'app-counter', standalone: true, template: `

Count: {{ count() }}

Double: {{ doubleCount() }}

` }) export class CounterComponent { // Writable signal count = signal(0); // Computed signal (read-only, auto-updates) doubleCount = computed(() => this.count() * 2); // Effect (runs when dependencies change) constructor() { effect(() => { console.log('Count changed:', this.count()); // Save to localStorage localStorage.setItem('count', this.count().toString()); }); } increment() { this.count.update(value => value + 1); } decrement() { this.count.update(value => value - 1); } reset() { this.count.set(0); } } ``` ### Complex State with Signals ```typescript import { Component, signal, computed } from '@angular/core'; interface Todo { id: number; title: string; completed: boolean; } @Component({ selector: 'app-todo-list', standalone: true, imports: [CommonModule, FormsModule], template: `
Total: {{ totalCount() }} | Active: {{ activeCount() }} | Completed: {{ completedCount() }}
    @for (todo of filteredTodos(); track todo.id) {
  • {{ todo.title }}
  • }
` }) export class TodoListComponent { todos = signal([]); filter = signal<'all' | 'active' | 'completed'>('all'); newTodoTitle = ''; // Computed signals totalCount = computed(() => this.todos().length); activeCount = computed(() => this.todos().filter(t => !t.completed).length ); completedCount = computed(() => this.todos().filter(t => t.completed).length ); filteredTodos = computed(() => { const todos = this.todos(); const filter = this.filter(); switch (filter) { case 'active': return todos.filter(t => !t.completed); case 'completed': return todos.filter(t => t.completed); default: return todos; } }); addTodo() { if (!this.newTodoTitle.trim()) return; this.todos.update(todos => [ ...todos, { id: Date.now(), title: this.newTodoTitle, completed: false } ]); this.newTodoTitle = ''; } toggleTodo(id: number) { this.todos.update(todos => todos.map(todo => todo.id === id ? { ...todo, completed: !todo.completed } : todo ) ); } removeTodo(id: number) { this.todos.update(todos => todos.filter(todo => todo.id !== id) ); } setFilter(filter: 'all' | 'active' | 'completed') { this.filter.set(filter); } } ``` ### Signals with Services ```typescript // user.service.ts import { Injectable, signal, computed } from '@angular/core'; import { HttpClient } from '@angular/common/http'; @Injectable({ providedIn: 'root' }) export class UserService { private http = inject(HttpClient); // Private writable signal private usersSignal = signal([]); // Public read-only computed signal users = this.usersSignal.asReadonly(); // Computed signals userCount = computed(() => this.usersSignal().length); activeUsers = computed(() => this.usersSignal().filter(u => u.isActive) ); async loadUsers() { const users = await firstValueFrom( this.http.get('/api/users') ); this.usersSignal.set(users); } addUser(user: User) { this.usersSignal.update(users => [...users, user]); } updateUser(id: string, updates: Partial) { this.usersSignal.update(users => users.map(user => user.id === id ? { ...user, ...updates } : user ) ); } removeUser(id: string) { this.usersSignal.update(users => users.filter(user => user.id !== id) ); } } ``` --- ## New Control Flow Syntax ### @if - Conditional Rendering ```typescript @Component({ template: ` @if (user()) { } @if (isLoggedIn()) { } @else { } @if (status() === 'loading') {
Loading...
} @else if (status() === 'error') {
Error occurred
} @else if (status() === 'success') {
{{ data() }}
} @else {
No data
} @if (user()) { @if (user()!.role === 'admin') { } } ` }) export class ExampleComponent { user = signal(null); isLoggedIn = signal(false); status = signal<'loading' | 'error' | 'success' | 'idle'>('idle'); data = signal(null); } ``` ### @for - List Rendering ```typescript @Component({ template: `
    @for (item of items(); track item.id) {
  • {{ item.name }}
  • }
    @for (item of items(); track item.id; let i = $index) {
  • {{ i + 1 }}. {{ item.name }}
  • }
    @for (item of items(); track item.id; let idx = $index, first = $first, last = $last) {
  • {{ idx }}: {{ item.name }}
  • }
    @for (item of items(); track item.id) {
  • {{ item.name }}
  • } @empty {
  • No items found
  • }
@for (category of categories(); track category.id) {

{{ category.name }}

    @for (product of category.products; track product.id) {
  • {{ product.name }}
  • }
} ` }) export class ListComponent { items = signal([]); categories = signal([]); } ``` ### @switch - Switch Statements ```typescript @Component({ template: ` @switch (userRole()) { @case ('admin') {
Admin Dashboard
} @case ('editor') {
Editor Dashboard
} @case ('viewer') {
Viewer Dashboard
} @default {
Guest View
} } @switch (status()) { @case ('active') { @if (isPremium()) {
Premium Active User
} @else {
Active User
} } @case ('inactive') {
Inactive User
} } ` }) export class SwitchComponent { userRole = signal<'admin' | 'editor' | 'viewer' | 'guest'>('guest'); status = signal<'active' | 'inactive'>('active'); isPremium = signal(false); } ``` --- ## Deferred Loading (@defer) ### Basic Deferred Loading ```typescript @Component({ template: `

Welcome

@defer { } @placeholder {
Chart will load...
} @loading (minimum 1s) {
Loading chart...
} @error {
Failed to load chart
}
` }) export class DashboardComponent { chartData = signal([]); } ``` ### Deferred Loading Triggers ```typescript @Component({ template: ` @defer (on viewport) { } @placeholder {
Scroll down to load users...
} @defer (on interaction) { } @placeholder {
Click to load comments
} @defer (on hover) { } @defer (on idle) { } @defer (on timer(5s)) { } @defer (when shouldLoad()) { } @defer (on interaction; prefetch on idle) { } @defer (on viewport; on timer(10s)) { } ` }) export class LazyLoadComponent { shouldLoad = signal(false); } ``` --- ## Dependency Injection ### Modern inject() Function ```typescript import { Component, inject } from '@angular/core'; @Component({ selector: 'app-user-profile', standalone: true, template: `...` }) export class UserProfileComponent { // Modern inject() - cleaner than constructor DI private userService = inject(UserService); private router = inject(Router); private activatedRoute = inject(ActivatedRoute); // Optional injection private analyticsService = inject(AnalyticsService, { optional: true }); // Self injection private elementRef = inject(ElementRef, { self: true }); async ngOnInit() { const userId = this.activatedRoute.snapshot.params['id']; const user = await this.userService.getUser(userId); this.analyticsService?.trackView('user-profile'); } } ``` ### Functional Guards ```typescript // auth.guard.ts import { inject } from '@angular/core'; import { Router } from '@angular/router'; import { AuthService } from '../services/auth.service'; export const authGuard = () => { const authService = inject(AuthService); const router = inject(Router); if (authService.isAuthenticated()) { return true; } return router.createUrlTree(['/login']); }; // Usage in routes export const routes: Routes = [ { path: 'dashboard', loadComponent: () => import('./dashboard.component'), canActivate: [authGuard] } ]; ``` ### Functional Interceptors ```typescript // auth.interceptor.ts import { HttpInterceptorFn } from '@angular/common/http'; import { inject } from '@angular/core'; import { AuthService } from '../services/auth.service'; export const authInterceptor: HttpInterceptorFn = (req, next) => { const authService = inject(AuthService); const token = authService.getToken(); if (token) { req = req.clone({ setHeaders: { Authorization: `Bearer ${token}` } }); } return next(req); }; // error.interceptor.ts export const errorInterceptor: HttpInterceptorFn = (req, next) => { return next(req).pipe( catchError((error: HttpErrorResponse) => { if (error.status === 401) { // Handle unauthorized } return throwError(() => error); }) ); }; // Bootstrap with interceptors bootstrapApplication(AppComponent, { providers: [ provideHttpClient( withInterceptors([authInterceptor, errorInterceptor]) ) ] }); ``` --- ## Input/Output Transforms ### Input Transforms ```typescript import { Component, Input, booleanAttribute, numberAttribute } from '@angular/core'; @Component({ selector: 'app-button', standalone: true, template: ` ` }) export class ButtonComponent { // Boolean transform - handles "", "true", "false" @Input({ transform: booleanAttribute }) disabled = false; // Number transform @Input({ transform: numberAttribute }) size = 16; // Custom transform @Input({ transform: (value: string) => value.toUpperCase() }) label = ''; } // Usage // Click me ``` ### Required Inputs ```typescript @Component({ selector: 'app-user-card', standalone: true, template: `

{{ user.name }}

{{ user.email }}

` }) export class UserCardComponent { // Required input - compile error if not provided @Input({ required: true }) user!: User; // Optional with default @Input() showActions = true; } ``` --- ## Server-Side Rendering (SSR) ### SSR-Compatible Component ```typescript import { Component, inject, PLATFORM_ID, afterNextRender } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Component({ selector: 'app-ssr-component', standalone: true, template: `
@if (isBrowser) {

Client-side only content

}

Universal content

` }) export class SsrComponent { private platformId = inject(PLATFORM_ID); isBrowser = isPlatformBrowser(this.platformId); constructor() { // Run only after render (browser only) afterNextRender(() => { console.log('Component rendered in browser'); this.initBrowserOnlyFeatures(); }); } private initBrowserOnlyFeatures() { // DOM manipulation, localStorage, etc. } } ``` --- ## Best Practices ### ✅ DO ```typescript // Use standalone components @Component({ standalone: true }) // Use signals for state count = signal(0); // Use new control flow @if (condition) { } @for (item of items; track item.id) { } // Use inject() for DI private service = inject(MyService); // Use @defer for lazy loading @defer (on viewport) { } // Use required inputs @Input({ required: true }) data!: Data; ``` ### ❌ DON'T ```typescript // Don't use NgModules for new code @NgModule({ }) // Use standalone instead // Don't use *ngIf/*ngFor
// Use @if instead // Don't use constructor DI when inject() is cleaner constructor(private service: MyService) // Use inject() // Don't load everything eagerly import { HeavyComponent } from './heavy'; // Use @defer or lazy routes ``` --- ## Migration Tips ### From Angular < 17 ```bash # Update to latest version ng update @angular/core @angular/cli # Convert to standalone ng generate @angular/core:standalone # Update control flow ng generate @angular/core:control-flow ``` --- ## Resources - **Angular Docs**: https://angular.dev - **Signals**: https://angular.dev/guide/signals - **Control Flow**: https://angular.dev/guide/templates/control-flow - **Standalone**: https://angular.dev/guide/components/importing --- ## Code Review Checklist - [ ] Components are standalone - [ ] Signals used for reactive state - [ ] New control flow syntax (@if/@for) - [ ] Proper track functions in @for - [ ] @defer used for performance - [ ] inject() used for DI - [ ] Required inputs marked - [ ] SSR compatibility considered - [ ] TypeScript strict mode enabled - [ ] Proper lazy loading strategy --- ## Communication Guidelines ### Prioritization ``` CRITICAL: Performance issues, SSR bugs, broken reactivity HIGH: Missing signals, old syntax usage, no lazy loading MEDIUM: Component organization, optimization opportunities LOW: Style improvements, minor refactoring ```