--- name: modern-angular-implementation description: Implement Angular 18+ features: Signals, standalone components, @defer blocks, SSR, zoneless change detection, new control flow syntax, and Material 3 integration. --- # Modern Angular Implementation Skill ## Angular Signals ### Basic Signals ```typescript import { Component, signal, computed, effect } from '@angular/core'; @Component({ selector: 'app-counter', standalone: true, template: `

Double: {{ double() }}

` }) export class CounterComponent { // Writable signal count = signal(0); // Computed signal (auto-updates) double = computed(() => this.count() * 2); constructor() { // Effect (side effects) effect(() => { console.log('Count changed:', this.count()); }); } increment() { this.count.update(n => n + 1); // or: this.count.set(this.count() + 1); } } ``` ### Signal Store Pattern ```typescript @Injectable({ providedIn: 'root' }) export class UserStore { // Private state signal private state = signal<{ users: User[]; loading: boolean; error: string | null; }>({ users: [], loading: false, error: null }); // Public computed selectors readonly users = computed(() => this.state().users); readonly loading = computed(() => this.state().loading); readonly error = computed(() => this.state().error); readonly userCount = computed(() => this.users().length); // Actions async loadUsers() { this.state.update(s => ({ ...s, loading: true })); try { const users = await this.http.get('/api/users'); this.state.update(s => ({ ...s, users, loading: false })); } catch (error) { this.state.update(s => ({ ...s, error: error.message, loading: false })); } } addUser(user: User) { this.state.update(s => ({ ...s, users: [...s.users, user] })); } } ``` ### Signals vs RxJS ```typescript // ❌ OLD: RxJS BehaviorSubject private userSubject = new BehaviorSubject(null); user$ = this.userSubject.asObservable(); userName$ = this.user$.pipe(map(u => u?.name ?? 'Guest')); ngOnDestroy() { this.userSubject.complete(); } // ✅ NEW: Angular Signals user = signal(null); userName = computed(() => this.user()?.name ?? 'Guest'); // No cleanup needed! ``` ## Standalone Components ### Basic Standalone Component ```typescript import { Component } from '@angular/core'; import { CommonModule } from '@angular/common'; import { RouterModule } from '@angular/router'; @Component({ selector: 'app-dashboard', standalone: true, imports: [CommonModule, RouterModule], // Import dependencies directly template: `

Dashboard

` }) export class DashboardComponent {} ``` ### Standalone 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'; import { routes } from './app/app.routes'; bootstrapApplication(AppComponent, { providers: [ provideRouter(routes), provideHttpClient(), // Add other providers ] }); ``` ### Standalone 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: 'users', loadChildren: () => import('./users/users.routes').then(m => m.USERS_ROUTES) } ]; ``` ### Migration Command ```bash # Automated migration to standalone ng generate @angular/core:standalone ``` ## Deferrable Views (@defer) ### Basic @defer ```typescript @defer { } @placeholder {
} ``` ### Defer with Triggers ```typescript // On viewport (when visible) @defer (on viewport) { } @placeholder {
} // On interaction (click or keydown) @defer (on interaction) { } @placeholder { } // On hover @defer (on hover) { } // On idle (browser idle) @defer (on idle) { } // On timer @defer (on timer(5s)) { } ``` ### Advanced @defer with States ```typescript @defer (on interaction; prefetch on idle) { } @loading (minimum 500ms; after 100ms) { } @placeholder (minimum 1s) { } @error {

Failed to load video player

} ``` ### Strategic Deferment ```typescript
@defer (on viewport) { } @defer (on viewport) { } @defer (on interaction; prefetch on idle) { } @placeholder { }
``` ## New Control Flow ### @if (replaces *ngIf) ```typescript // OLD
{{ user.name }}
{{ user.name }}
// NEW @if (user) {
{{ user.name }}
} @if (user) {
{{ user.name }}
} @else {
Loading...
} ``` ### @for (replaces *ngFor) ```typescript // OLD
{{ item.name }}
// NEW @for (item of items; track item.id) {
{{ item.name }}
} @empty {

No items found

} ``` ### @switch (replaces *ngSwitch) ```typescript // OLD

Loading...

Error occurred

Success

// NEW @switch (status) { @case ('loading') {

Loading...

} @case ('error') {

Error occurred

} @default {

Success

} } ``` ### Combined Control Flow ```typescript @if (users.length > 0) {
    @for (user of users; track user.id) {
  • {{ user.name }} @if (user.isAdmin) { Admin }
  • } @empty {
  • No users found
  • }
} @else {

Loading users...

} ``` ## Server-Side Rendering (SSR) ### Enable SSR ```bash # Add SSR to existing project ng add @angular/ssr # Or create new project with SSR ng new my-app --ssr ``` ### SSR Configuration ```typescript // app.config.ts import { ApplicationConfig } from '@angular/core'; import { provideClientHydration } from '@angular/platform-browser'; export const appConfig: ApplicationConfig = { providers: [ provideClientHydration() // Enable hydration ] }; ``` ### SSR-Safe Code ```typescript import { Component, Inject, PLATFORM_ID } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Component({...}) export class MapComponent { constructor(@Inject(PLATFORM_ID) private platformId: Object) {} ngOnInit() { // Only run in browser if (isPlatformBrowser(this.platformId)) { this.initializeMap(); this.loadGoogleMapsAPI(); } } private initializeMap() { // Browser-specific code const map = new google.maps.Map(document.getElementById('map')); } } ``` ### Transfer State (Avoid Duplicate Requests) ```typescript import { Component, makeStateKey, TransferState } from '@angular/core'; const USERS_KEY = makeStateKey('users'); @Component({...}) export class UsersComponent { constructor( private http: HttpClient, private transferState: TransferState ) {} loadUsers() { // Check if data exists in transfer state (from SSR) const users = this.transferState.get(USERS_KEY, null); if (users) { // Use cached data from SSR return of(users); } // Fetch from API and cache for hydration return this.http.get('/api/users').pipe( tap(users => this.transferState.set(USERS_KEY, users)) ); } } ``` ## Zoneless Change Detection ### Enable Zoneless ```typescript // app.config.ts import { ApplicationConfig } from '@angular/core'; import { provideExperimentalZonelessChangeDetection } from '@angular/core'; export const appConfig: ApplicationConfig = { providers: [ provideExperimentalZonelessChangeDetection() ] }; ``` ### Zoneless-Compatible Code ```typescript @Component({...}) export class MyComponent { count = signal(0); // Signals work great with zoneless! // Manual change detection when needed constructor(private cdr: ChangeDetectorRef) {} onManualUpdate() { this.legacyProperty = 'new value'; this.cdr.markForCheck(); // Trigger change detection manually } } ``` ## Material 3 ### Install Material 3 ```bash ng add @angular/material ``` ### Material 3 Theme ```scss // styles.scss @use '@angular/material' as mat; $my-theme: mat.define-theme(( color: ( theme-type: light, primary: mat.$azure-palette, ), )); html { @include mat.all-component-themes($my-theme); } // Dark mode html.dark-theme { $dark-theme: mat.define-theme(( color: ( theme-type: dark, primary: mat.$azure-palette, ), )); @include mat.all-component-colors($dark-theme); } ``` ### Material 3 Components ```typescript import { Component } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatIconModule } from '@angular/material/icon'; @Component({ standalone: true, imports: [MatButtonModule, MatCardModule, MatIconModule], template: ` Material 3 Card

Beautiful Material Design 3 components

` }) export class MaterialCardComponent {} ``` ## Migration Patterns ### NgModule → Standalone ```typescript // BEFORE: NgModule @NgModule({ declarations: [UserComponent, UserListComponent], imports: [CommonModule, RouterModule], exports: [UserComponent] }) export class UserModule {} // AFTER: Standalone export const USER_ROUTES: Routes = [{ path: '', loadComponent: () => import('./user.component').then(m => m.UserComponent) }]; @Component({ standalone: true, imports: [CommonModule, RouterModule] }) export class UserComponent {} ``` ### RxJS → Signals ```typescript // BEFORE: RxJS class UserService { private usersSubject = new BehaviorSubject([]); users$ = this.usersSubject.asObservable(); addUser(user: User) { const current = this.usersSubject.value; this.usersSubject.next([...current, user]); } } // AFTER: Signals class UserService { users = signal([]); addUser(user: User) { this.users.update(users => [...users, user]); } } ``` ## Performance Optimization ### Bundle Size Reduction with @defer ```typescript // Can reduce initial bundle by 40-60%! @defer (on viewport) { } ``` ### Zoneless Performance Gains ```typescript // 20-30% performance improvement provideExperimentalZonelessChangeDetection() ``` ### SSR Core Web Vitals ```typescript // Dramatically improves LCP, FCP, TTFB provideClientHydration() ``` ## Best Practices 1. **Use Signals for Simple State** - Perfect for component-local reactive state 2. **Keep RxJS for Complex Async** - Still best for HTTP, WebSockets, complex operators 3. **Strategic @defer** - Don't defer critical content, be strategic 4. **Gradual Migration** - Migrate to standalone incrementally 5. **SSR-Safe Guards** - Always check isPlatformBrowser for DOM access 6. **Zoneless-Ready** - Use Signals and OnPush to prepare for zoneless future ## Resources - [Signals Documentation](https://angular.dev/guide/signals) - [Standalone Migration](https://angular.dev/reference/migrations/standalone) - [@defer Guide](https://angular.dev/guide/templates/defer) - [SSR Guide](https://angular.dev/guide/ssr) - [Material 3](https://material.angular.io)