Add this skill
npx mdskills install sickn33/angularComprehensive modern Angular reference with Signals, Zoneless, SSR, and practical code examples
1---2name: angular3description: >-4 Modern Angular (v20+) expert with deep knowledge of Signals, Standalone5 Components, Zoneless applications, SSR/Hydration, and reactive patterns.6 Use PROACTIVELY for Angular development, component architecture, state7 management, performance optimization, and migration to modern patterns.8risk: safe9source: self10---1112# Angular Expert1314Master modern Angular development with Signals, Standalone Components, Zoneless applications, SSR/Hydration, and the latest reactive patterns.1516## When to Use This Skill1718- Building new Angular applications (v20+)19- Implementing Signals-based reactive patterns20- Creating Standalone Components and migrating from NgModules21- Configuring Zoneless Angular applications22- Implementing SSR, prerendering, and hydration23- Optimizing Angular performance24- Adopting modern Angular patterns and best practices2526## Do Not Use This Skill When2728- Migrating from AngularJS (1.x) → use `angular-migration` skill29- Working with legacy Angular apps that cannot upgrade30- General TypeScript issues → use `typescript-expert` skill3132## Instructions33341. Assess the Angular version and project structure352. Apply modern patterns (Signals, Standalone, Zoneless)363. Implement with proper typing and reactivity374. Validate with build and tests3839## Safety4041- Always test changes in development before production42- Gradual migration for existing apps (don't big-bang refactor)43- Keep backward compatibility during transitions4445---4647## Angular Version Timeline4849| Version | Release | Key Features |50| -------------- | ------- | ------------------------------------------------------ |51| **Angular 20** | Q2 2025 | Signals stable, Zoneless stable, Incremental hydration |52| **Angular 21** | Q4 2025 | Signals-first default, Enhanced SSR |53| **Angular 22** | Q2 2026 | Signal Forms, Selectorless components |5455---5657## 1. Signals: The New Reactive Primitive5859Signals are Angular's fine-grained reactivity system, replacing zone.js-based change detection.6061### Core Concepts6263```typescript64import { signal, computed, effect } from "@angular/core";6566// Writable signal67const count = signal(0);6869// Read value70console.log(count()); // 07172// Update value73count.set(5); // Direct set74count.update((v) => v + 1); // Functional update7576// Computed (derived) signal77const doubled = computed(() => count() * 2);7879// Effect (side effects)80effect(() => {81 console.log(`Count changed to: ${count()}`);82});83```8485### Signal-Based Inputs and Outputs8687```typescript88import { Component, input, output, model } from "@angular/core";8990@Component({91 selector: "app-user-card",92 standalone: true,93 template: `94 <div class="card">95 <h3>{{ name() }}</h3>96 <span>{{ role() }}</span>97 <button (click)="select.emit(id())">Select</button>98 </div>99 `,100})101export class UserCardComponent {102 // Signal inputs (read-only)103 id = input.required<string>();104 name = input.required<string>();105 role = input<string>("User"); // With default106107 // Output108 select = output<string>();109110 // Two-way binding (model)111 isSelected = model(false);112}113114// Usage:115// <app-user-card [id]="'123'" [name]="'John'" [(isSelected)]="selected" />116```117118### Signal Queries (ViewChild/ContentChild)119120```typescript121import {122 Component,123 viewChild,124 viewChildren,125 contentChild,126} from "@angular/core";127128@Component({129 selector: "app-container",130 standalone: true,131 template: `132 <input #searchInput />133 <app-item *ngFor="let item of items()" />134 `,135})136export class ContainerComponent {137 // Signal-based queries138 searchInput = viewChild<ElementRef>("searchInput");139 items = viewChildren(ItemComponent);140 projectedContent = contentChild(HeaderDirective);141142 focusSearch() {143 this.searchInput()?.nativeElement.focus();144 }145}146```147148### When to Use Signals vs RxJS149150| Use Case | Signals | RxJS |151| ----------------------- | --------------- | -------------------------------- |152| Local component state | ✅ Preferred | Overkill |153| Derived/computed values | ✅ `computed()` | `combineLatest` works |154| Side effects | ✅ `effect()` | `tap` operator |155| HTTP requests | ❌ | ✅ HttpClient returns Observable |156| Event streams | ❌ | ✅ `fromEvent`, operators |157| Complex async flows | ❌ | ✅ `switchMap`, `mergeMap` |158159---160161## 2. Standalone Components162163Standalone components are self-contained and don't require NgModule declarations.164165### Creating Standalone Components166167```typescript168import { Component } from "@angular/core";169import { CommonModule } from "@angular/common";170import { RouterLink } from "@angular/router";171172@Component({173 selector: "app-header",174 standalone: true,175 imports: [CommonModule, RouterLink], // Direct imports176 template: `177 <header>178 <a routerLink="/">Home</a>179 <a routerLink="/about">About</a>180 </header>181 `,182})183export class HeaderComponent {}184```185186### Bootstrapping Without NgModule187188```typescript189// main.ts190import { bootstrapApplication } from "@angular/platform-browser";191import { provideRouter } from "@angular/router";192import { provideHttpClient } from "@angular/common/http";193import { AppComponent } from "./app/app.component";194import { routes } from "./app/app.routes";195196bootstrapApplication(AppComponent, {197 providers: [provideRouter(routes), provideHttpClient()],198});199```200201### Lazy Loading Standalone Components202203```typescript204// app.routes.ts205import { Routes } from "@angular/router";206207export const routes: Routes = [208 {209 path: "dashboard",210 loadComponent: () =>211 import("./dashboard/dashboard.component").then(212 (m) => m.DashboardComponent,213 ),214 },215 {216 path: "admin",217 loadChildren: () =>218 import("./admin/admin.routes").then((m) => m.ADMIN_ROUTES),219 },220];221```222223---224225## 3. Zoneless Angular226227Zoneless applications don't use zone.js, improving performance and debugging.228229### Enabling Zoneless Mode230231```typescript232// main.ts233import { bootstrapApplication } from "@angular/platform-browser";234import { provideZonelessChangeDetection } from "@angular/core";235import { AppComponent } from "./app/app.component";236237bootstrapApplication(AppComponent, {238 providers: [provideZonelessChangeDetection()],239});240```241242### Zoneless Component Patterns243244```typescript245import { Component, signal, ChangeDetectionStrategy } from "@angular/core";246247@Component({248 selector: "app-counter",249 standalone: true,250 changeDetection: ChangeDetectionStrategy.OnPush,251 template: `252 <div>Count: {{ count() }}</div>253 <button (click)="increment()">+</button>254 `,255})256export class CounterComponent {257 count = signal(0);258259 increment() {260 this.count.update((v) => v + 1);261 // No zone.js needed - Signal triggers change detection262 }263}264```265266### Key Zoneless Benefits267268- **Performance**: No zone.js patches on async APIs269- **Debugging**: Clean stack traces without zone wrappers270- **Bundle size**: Smaller without zone.js (~15KB savings)271- **Interoperability**: Better with Web Components and micro-frontends272273---274275## 4. Server-Side Rendering & Hydration276277### SSR Setup with Angular CLI278279```bash280ng add @angular/ssr281```282283### Hydration Configuration284285```typescript286// app.config.ts287import { ApplicationConfig } from "@angular/core";288import {289 provideClientHydration,290 withEventReplay,291} from "@angular/platform-browser";292293export const appConfig: ApplicationConfig = {294 providers: [provideClientHydration(withEventReplay())],295};296```297298### Incremental Hydration (v20+)299300```typescript301import { Component } from "@angular/core";302303@Component({304 selector: "app-page",305 standalone: true,306 template: `307 <app-hero />308309 @defer (hydrate on viewport) {310 <app-comments />311 }312313 @defer (hydrate on interaction) {314 <app-chat-widget />315 }316 `,317})318export class PageComponent {}319```320321### Hydration Triggers322323| Trigger | When to Use |324| ---------------- | --------------------------------------- |325| `on idle` | Low-priority, hydrate when browser idle |326| `on viewport` | Hydrate when element enters viewport |327| `on interaction` | Hydrate on first user interaction |328| `on hover` | Hydrate when user hovers |329| `on timer(ms)` | Hydrate after specified delay |330331---332333## 5. Modern Routing Patterns334335### Functional Route Guards336337```typescript338// auth.guard.ts339import { inject } from "@angular/core";340import { Router, CanActivateFn } from "@angular/router";341import { AuthService } from "./auth.service";342343export const authGuard: CanActivateFn = (route, state) => {344 const auth = inject(AuthService);345 const router = inject(Router);346347 if (auth.isAuthenticated()) {348 return true;349 }350351 return router.createUrlTree(["/login"], {352 queryParams: { returnUrl: state.url },353 });354};355356// Usage in routes357export const routes: Routes = [358 {359 path: "dashboard",360 loadComponent: () => import("./dashboard.component"),361 canActivate: [authGuard],362 },363];364```365366### Route-Level Data Resolvers367368```typescript369import { inject } from '@angular/core';370import { ResolveFn } from '@angular/router';371import { UserService } from './user.service';372import { User } from './user.model';373374export const userResolver: ResolveFn<User> = (route) => {375 const userService = inject(UserService);376 return userService.getUser(route.paramMap.get('id')!);377};378379// In routes380{381 path: 'user/:id',382 loadComponent: () => import('./user.component'),383 resolve: { user: userResolver }384}385386// In component387export class UserComponent {388 private route = inject(ActivatedRoute);389 user = toSignal(this.route.data.pipe(map(d => d['user'])));390}391```392393---394395## 6. Dependency Injection Patterns396397### Modern inject() Function398399```typescript400import { Component, inject } from '@angular/core';401import { HttpClient } from '@angular/common/http';402import { UserService } from './user.service';403404@Component({...})405export class UserComponent {406 // Modern inject() - no constructor needed407 private http = inject(HttpClient);408 private userService = inject(UserService);409410 // Works in any injection context411 users = toSignal(this.userService.getUsers());412}413```414415### Injection Tokens for Configuration416417```typescript418import { InjectionToken, inject } from "@angular/core";419420// Define token421export const API_BASE_URL = new InjectionToken<string>("API_BASE_URL");422423// Provide in config424bootstrapApplication(AppComponent, {425 providers: [{ provide: API_BASE_URL, useValue: "https://api.example.com" }],426});427428// Inject in service429@Injectable({ providedIn: "root" })430export class ApiService {431 private baseUrl = inject(API_BASE_URL);432433 get(endpoint: string) {434 return this.http.get(`${this.baseUrl}/${endpoint}`);435 }436}437```438439---440441## 7. Component Composition & Reusability442443### Content Projection (Slots)444445```typescript446@Component({447 selector: 'app-card',448 template: `449 <div class="card">450 <div class="header">451 <!-- Select by attribute -->452 <ng-content select="[card-header]"></ng-content>453 </div>454 <div class="body">455 <!-- Default slot -->456 <ng-content></ng-content>457 </div>458 </div>459 `460})461export class CardComponent {}462463// Usage464<app-card>465 <h3 card-header>Title</h3>466 <p>Body content</p>467</app-card>468```469470### Host Directives (Composition)471472```typescript473// Reusable behaviors without inheritance474@Directive({475 standalone: true,476 selector: '[appTooltip]',477 inputs: ['tooltip'] // Signal input alias478})479export class TooltipDirective { ... }480481@Component({482 selector: 'app-button',483 standalone: true,484 hostDirectives: [485 {486 directive: TooltipDirective,487 inputs: ['tooltip: title'] // Map input488 }489 ],490 template: `<ng-content />`491})492export class ButtonComponent {}493```494495---496497## 8. State Management Patterns498499### Signal-Based State Service500501```typescript502import { Injectable, signal, computed } from "@angular/core";503504interface AppState {505 user: User | null;506 theme: "light" | "dark";507 notifications: Notification[];508}509510@Injectable({ providedIn: "root" })511export class StateService {512 // Private writable signals513 private _user = signal<User | null>(null);514 private _theme = signal<"light" | "dark">("light");515 private _notifications = signal<Notification[]>([]);516517 // Public read-only computed518 readonly user = computed(() => this._user());519 readonly theme = computed(() => this._theme());520 readonly notifications = computed(() => this._notifications());521 readonly unreadCount = computed(522 () => this._notifications().filter((n) => !n.read).length,523 );524525 // Actions526 setUser(user: User | null) {527 this._user.set(user);528 }529530 toggleTheme() {531 this._theme.update((t) => (t === "light" ? "dark" : "light"));532 }533534 addNotification(notification: Notification) {535 this._notifications.update((n) => [...n, notification]);536 }537}538```539540### Component Store Pattern with Signals541542```typescript543import { Injectable, signal, computed, inject } from "@angular/core";544import { HttpClient } from "@angular/common/http";545import { toSignal } from "@angular/core/rxjs-interop";546547@Injectable()548export class ProductStore {549 private http = inject(HttpClient);550551 // State552 private _products = signal<Product[]>([]);553 private _loading = signal(false);554 private _filter = signal("");555556 // Selectors557 readonly products = computed(() => this._products());558 readonly loading = computed(() => this._loading());559 readonly filteredProducts = computed(() => {560 const filter = this._filter().toLowerCase();561 return this._products().filter((p) =>562 p.name.toLowerCase().includes(filter),563 );564 });565566 // Actions567 loadProducts() {568 this._loading.set(true);569 this.http.get<Product[]>("/api/products").subscribe({570 next: (products) => {571 this._products.set(products);572 this._loading.set(false);573 },574 error: () => this._loading.set(false),575 });576 }577578 setFilter(filter: string) {579 this._filter.set(filter);580 }581}582```583584---585586## 9. Forms with Signals (Coming in v22+)587588### Current Reactive Forms589590```typescript591import { Component, inject } from "@angular/core";592import { FormBuilder, Validators, ReactiveFormsModule } from "@angular/forms";593594@Component({595 selector: "app-user-form",596 standalone: true,597 imports: [ReactiveFormsModule],598 template: `599 <form [formGroup]="form" (ngSubmit)="onSubmit()">600 <input formControlName="name" placeholder="Name" />601 <input formControlName="email" type="email" placeholder="Email" />602 <button [disabled]="form.invalid">Submit</button>603 </form>604 `,605})606export class UserFormComponent {607 private fb = inject(FormBuilder);608609 form = this.fb.group({610 name: ["", Validators.required],611 email: ["", [Validators.required, Validators.email]],612 });613614 onSubmit() {615 if (this.form.valid) {616 console.log(this.form.value);617 }618 }619}620```621622### Signal-Aware Form Patterns (Preview)623624```typescript625// Future Signal Forms API (experimental)626import { Component, signal } from '@angular/core';627628@Component({...})629export class SignalFormComponent {630 name = signal('');631 email = signal('');632633 // Computed validation634 isValid = computed(() =>635 this.name().length > 0 &&636 this.email().includes('@')637 );638639 submit() {640 if (this.isValid()) {641 console.log({ name: this.name(), email: this.email() });642 }643 }644}645```646647---648649## 10. Performance Optimization650651### Change Detection Strategies652653```typescript654@Component({655 changeDetection: ChangeDetectionStrategy.OnPush,656 // Only checks when:657 // 1. Input signal/reference changes658 // 2. Event handler runs659 // 3. Async pipe emits660 // 4. Signal value changes661})662```663664### Defer Blocks for Lazy Loading665666```typescript667@Component({668 template: `669 <!-- Immediate loading -->670 <app-header />671672 <!-- Lazy load when visible -->673 @defer (on viewport) {674 <app-heavy-chart />675 } @placeholder {676 <div class="skeleton" />677 } @loading (minimum 200ms) {678 <app-spinner />679 } @error {680 <p>Failed to load chart</p>681 }682 `683})684```685686### NgOptimizedImage687688```typescript689import { NgOptimizedImage } from '@angular/common';690691@Component({692 imports: [NgOptimizedImage],693 template: `694 <img695 ngSrc="hero.jpg"696 width="800"697 height="600"698 priority699 />700701 <img702 ngSrc="thumbnail.jpg"703 width="200"704 height="150"705 loading="lazy"706 placeholder="blur"707 />708 `709})710```711712---713714## 11. Testing Modern Angular715716### Testing Signal Components717718```typescript719import { ComponentFixture, TestBed } from "@angular/core/testing";720import { CounterComponent } from "./counter.component";721722describe("CounterComponent", () => {723 let component: CounterComponent;724 let fixture: ComponentFixture<CounterComponent>;725726 beforeEach(async () => {727 await TestBed.configureTestingModule({728 imports: [CounterComponent], // Standalone import729 }).compileComponents();730731 fixture = TestBed.createComponent(CounterComponent);732 component = fixture.componentInstance;733 fixture.detectChanges();734 });735736 it("should increment count", () => {737 expect(component.count()).toBe(0);738739 component.increment();740741 expect(component.count()).toBe(1);742 });743744 it("should update DOM on signal change", () => {745 component.count.set(5);746 fixture.detectChanges();747748 const el = fixture.nativeElement.querySelector(".count");749 expect(el.textContent).toContain("5");750 });751});752```753754### Testing with Signal Inputs755756```typescript757import { ComponentFixture, TestBed } from "@angular/core/testing";758import { ComponentRef } from "@angular/core";759import { UserCardComponent } from "./user-card.component";760761describe("UserCardComponent", () => {762 let fixture: ComponentFixture<UserCardComponent>;763 let componentRef: ComponentRef<UserCardComponent>;764765 beforeEach(async () => {766 await TestBed.configureTestingModule({767 imports: [UserCardComponent],768 }).compileComponents();769770 fixture = TestBed.createComponent(UserCardComponent);771 componentRef = fixture.componentRef;772773 // Set signal inputs via setInput774 componentRef.setInput("id", "123");775 componentRef.setInput("name", "John Doe");776777 fixture.detectChanges();778 });779780 it("should display user name", () => {781 const el = fixture.nativeElement.querySelector("h3");782 expect(el.textContent).toContain("John Doe");783 });784});785```786787---788789## Best Practices Summary790791| Pattern | ✅ Do | ❌ Don't |792| -------------------- | ------------------------------ | ------------------------------- |793| **State** | Use Signals for local state | Overuse RxJS for simple state |794| **Components** | Standalone with direct imports | Bloated SharedModules |795| **Change Detection** | OnPush + Signals | Default CD everywhere |796| **Lazy Loading** | `@defer` and `loadComponent` | Eager load everything |797| **DI** | `inject()` function | Constructor injection (verbose) |798| **Inputs** | `input()` signal function | `@Input()` decorator (legacy) |799| **Zoneless** | Enable for new projects | Force on legacy without testing |800801---802803## Resources804805- [Angular.dev Documentation](https://angular.dev)806- [Angular Signals Guide](https://angular.dev/guide/signals)807- [Angular SSR Guide](https://angular.dev/guide/ssr)808- [Angular Update Guide](https://angular.dev/update-guide)809- [Angular Blog](https://blog.angular.dev)810811---812813## Common Troubleshooting814815| Issue | Solution |816| ------------------------------ | --------------------------------------------------- |817| Signal not updating UI | Ensure `OnPush` + call signal as function `count()` |818| Hydration mismatch | Check server/client content consistency |819| Circular dependency | Use `inject()` with `forwardRef` |820| Zoneless not detecting changes | Trigger via signal updates, not mutations |821| SSR fetch fails | Use `TransferState` or `withFetch()` |822
Full transparency — inspect the skill content before installing.