Agent skill

angular-17-plus-specialist

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.

Stars 163
Forks 31

Install this agent skill to your Project

npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/development/angular-17-plus-specialist

SKILL.md

First read best pratices for angular in

best-practices.md SKILL

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: `
    <div class="profile">
      @if (loading()) {
        <div class="spinner">Loading...</div>
      } @else if (error()) {
        <div class="error">{{ error() }}</div>
      } @else if (user()) {
        <div class="profile-content">
          <h2>{{ user()!.name }}</h2>
          <p>{{ user()!.email }}</p>
          <button (click)="refresh()">Refresh</button>
        </div>
      }
    </div>
  `,
  styles: [`
    .profile {
      padding: 20px;
    }
    
    .spinner {
      text-align: center;
    }
    
    .error {
      color: red;
    }
  `]
})
export class UserProfileComponent {
  private userService = inject(UserService);
  
  // Signals for reactive state
  user = signal<User | null>(null);
  loading = signal(false);
  error = signal<string | null>(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: `
    <div class="counter">
      <h2>Count: {{ count() }}</h2>
      <h3>Double: {{ doubleCount() }}</h3>
      <button (click)="increment()">+</button>
      <button (click)="decrement()">-</button>
      <button (click)="reset()">Reset</button>
    </div>
  `
})
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: `
    <div class="todo-app">
      <input 
        [(ngModel)]="newTodoTitle"
        (keyup.enter)="addTodo()"
        placeholder="Add todo...">
      
      <div class="stats">
        Total: {{ totalCount() }} | 
        Active: {{ activeCount() }} | 
        Completed: {{ completedCount() }}
      </div>

      <ul>
        @for (todo of filteredTodos(); track todo.id) {
          <li [class.completed]="todo.completed">
            <input 
              type="checkbox"
              [checked]="todo.completed"
              (change)="toggleTodo(todo.id)">
            <span>{{ todo.title }}</span>
            <button (click)="removeTodo(todo.id)">×</button>
          </li>
        }
      </ul>

      <div class="filters">
        <button (click)="setFilter('all')">All</button>
        <button (click)="setFilter('active')">Active</button>
        <button (click)="setFilter('completed')">Completed</button>
      </div>
    </div>
  `
})
export class TodoListComponent {
  todos = signal<Todo[]>([]);
  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<User[]>([]);
  
  // 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<User[]>('/api/users')
    );
    this.usersSignal.set(users);
  }

  addUser(user: User) {
    this.usersSignal.update(users => [...users, user]);
  }

  updateUser(id: string, updates: Partial<User>) {
    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: `
    <!-- Simple if -->
    @if (user()) {
      <div class="user-info">
        <p>Welcome, {{ user()!.name }}</p>
      </div>
    }

    <!-- if-else -->
    @if (isLoggedIn()) {
      <button (click)="logout()">Logout</button>
    } @else {
      <button (click)="login()">Login</button>
    }

    <!-- if-else if-else -->
    @if (status() === 'loading') {
      <div class="spinner">Loading...</div>
    } @else if (status() === 'error') {
      <div class="error">Error occurred</div>
    } @else if (status() === 'success') {
      <div class="content">{{ data() }}</div>
    } @else {
      <div>No data</div>
    }

    <!-- Nested conditions -->
    @if (user()) {
      @if (user()!.role === 'admin') {
        <button>Admin Panel</button>
      }
    }
  `
})
export class ExampleComponent {
  user = signal<User | null>(null);
  isLoggedIn = signal(false);
  status = signal<'loading' | 'error' | 'success' | 'idle'>('idle');
  data = signal<any>(null);
}

@for - List Rendering

typescript
@Component({
  template: `
    <!-- Basic @for -->
    <ul>
      @for (item of items(); track item.id) {
        <li>{{ item.name }}</li>
      }
    </ul>

    <!-- With index -->
    <ul>
      @for (item of items(); track item.id; let i = $index) {
        <li>{{ i + 1 }}. {{ item.name }}</li>
      }
    </ul>

    <!-- With context variables -->
    <ul>
      @for (item of items(); track item.id; let idx = $index, first = $first, last = $last) {
        <li [class.first]="first" [class.last]="last">
          {{ idx }}: {{ item.name }}
        </li>
      }
    </ul>

    <!-- Empty block -->
    <ul>
      @for (item of items(); track item.id) {
        <li>{{ item.name }}</li>
      } @empty {
        <li class="empty">No items found</li>
      }
    </ul>

    <!-- Nested loops -->
    @for (category of categories(); track category.id) {
      <div class="category">
        <h3>{{ category.name }}</h3>
        <ul>
          @for (product of category.products; track product.id) {
            <li>{{ product.name }}</li>
          }
        </ul>
      </div>
    }
  `
})
export class ListComponent {
  items = signal<Item[]>([]);
  categories = signal<Category[]>([]);
}

@switch - Switch Statements

typescript
@Component({
  template: `
    <!-- Switch case -->
    @switch (userRole()) {
      @case ('admin') {
        <div class="admin-panel">Admin Dashboard</div>
      }
      @case ('editor') {
        <div class="editor-panel">Editor Dashboard</div>
      }
      @case ('viewer') {
        <div class="viewer-panel">Viewer Dashboard</div>
      }
      @default {
        <div class="guest-panel">Guest View</div>
      }
    }

    <!-- With @if inside cases -->
    @switch (status()) {
      @case ('active') {
        @if (isPremium()) {
          <div>Premium Active User</div>
        } @else {
          <div>Active User</div>
        }
      }
      @case ('inactive') {
        <div>Inactive User</div>
      }
    }
  `
})
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: `
    <div class="page">
      <!-- Immediate content -->
      <h1>Welcome</h1>
      
      <!-- Defer loading heavy component -->
      @defer {
        <app-heavy-chart [data]="chartData()"></app-heavy-chart>
      } @placeholder {
        <div class="placeholder">Chart will load...</div>
      } @loading (minimum 1s) {
        <div class="spinner">Loading chart...</div>
      } @error {
        <div class="error">Failed to load chart</div>
      }
    </div>
  `
})
export class DashboardComponent {
  chartData = signal<ChartData[]>([]);
}

Deferred Loading Triggers

typescript
@Component({
  template: `
    <!-- Load on viewport (intersection observer) -->
    @defer (on viewport) {
      <app-user-list></app-user-list>
    } @placeholder {
      <div>Scroll down to load users...</div>
    }

    <!-- Load on interaction -->
    @defer (on interaction) {
      <app-comments></app-comments>
    } @placeholder {
      <div>Click to load comments</div>
    }

    <!-- Load on hover -->
    @defer (on hover) {
      <app-tooltip></app-tooltip>
    }

    <!-- Load on idle -->
    @defer (on idle) {
      <app-analytics></app-analytics>
    }

    <!-- Load on timer -->
    @defer (on timer(5s)) {
      <app-notification></app-notification>
    }

    <!-- Load when signal changes -->
    @defer (when shouldLoad()) {
      <app-content></app-content>
    }

    <!-- Prefetch strategies -->
    @defer (on interaction; prefetch on idle) {
      <app-heavy-component></app-heavy-component>
    }

    <!-- Multiple triggers -->
    @defer (on viewport; on timer(10s)) {
      <app-lazy-content></app-lazy-content>
    }
  `
})
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: `
    <button 
      [disabled]="disabled"
      [style.font-size.px]="size">
      <ng-content></ng-content>
    </button>
  `
})
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
// <app-button disabled size="18">Click me</app-button>

Required Inputs

typescript
@Component({
  selector: 'app-user-card',
  standalone: true,
  template: `
    <div class="card">
      <h3>{{ user.name }}</h3>
      <p>{{ user.email }}</p>
    </div>
  `
})
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: `
    <div>
      @if (isBrowser) {
        <p>Client-side only content</p>
      }
      <p>Universal content</p>
    </div>
  `
})
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
<div *ngIf="condition"> // 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


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

Didn't find tool you were looking for?

Be as detailed as possible for better results