askill
angular-17-plus-specialist

angular-17-plus-specialistSafety 95Repository

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.

0 stars
1.2k downloads
Updated 2/5/2026

Package Files

Loading files...
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

// 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

// 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

// 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

// 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

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

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

// 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

@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

@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

@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

@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

@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

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

// 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

// 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

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

@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

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

// 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

// 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

# 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

Install

Download ZIP
Requires askill CLI v1.0+

AI Quality Score

95/100Analyzed 2/7/2026

An exceptionally high-quality technical reference for modern Angular development. It provides clear directives and comprehensive code examples for Signals, Standalone components, and the new control flow syntax.

95
92
100
90
98

Metadata

Licenseunknown
Version1.0.0
Updated2/5/2026
Publishermajiayu000

Tags

apisecurity