diff --git a/skills/angular-best-practices/SKILL.md b/skills/angular-best-practices/SKILL.md
new file mode 100644
index 00000000..9b375384
--- /dev/null
+++ b/skills/angular-best-practices/SKILL.md
@@ -0,0 +1,502 @@
+---
+name: angular-best-practices
+description: Angular performance optimization and best practices guide. Use when writing, reviewing, or refactoring Angular code for optimal performance, bundle size, and rendering efficiency.
+---
+
+# Angular Best Practices
+
+Comprehensive performance optimization guide for Angular applications. Contains prioritized rules for eliminating performance bottlenecks, optimizing bundles, and improving rendering.
+
+## When to Apply
+
+Reference these guidelines when:
+
+- Writing new Angular components or pages
+- Implementing data fetching patterns
+- Reviewing code for performance issues
+- Refactoring existing Angular code
+- Optimizing bundle size or load times
+- Configuring SSR/hydration
+
+---
+
+## Rule Categories by Priority
+
+| Priority | Category | Impact | Focus |
+| -------- | --------------------- | ---------- | ------------------------------- |
+| 1 | Change Detection | CRITICAL | Signals, OnPush, Zoneless |
+| 2 | Bundle Optimization | CRITICAL | Lazy loading, tree shaking |
+| 3 | Rendering Performance | HIGH | @defer, trackBy, virtualization |
+| 4 | Server-Side Rendering | HIGH | Hydration, prerendering |
+| 5 | Template Optimization | MEDIUM | Control flow, pipes |
+| 6 | State Management | MEDIUM | Signal patterns, selectors |
+| 7 | Memory Management | LOW-MEDIUM | Cleanup, subscriptions |
+
+---
+
+## 1. Change Detection (CRITICAL)
+
+### Use OnPush Change Detection
+
+```typescript
+// CORRECT - OnPush with Signals
+@Component({
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ template: `
{{ count() }}
`,
+})
+export class CounterComponent {
+ count = signal(0);
+}
+
+// WRONG - Default change detection
+@Component({
+ template: `{{ count }}
`, // Checked every cycle
+})
+export class CounterComponent {
+ count = 0;
+}
+```
+
+### Prefer Signals Over Mutable Properties
+
+```typescript
+// CORRECT - Signals trigger precise updates
+@Component({
+ template: `
+ {{ title() }}
+ Count: {{ count() }}
+ `,
+})
+export class DashboardComponent {
+ title = signal("Dashboard");
+ count = signal(0);
+}
+
+// WRONG - Mutable properties require zone.js checks
+@Component({
+ template: `
+ {{ title }}
+ Count: {{ count }}
+ `,
+})
+export class DashboardComponent {
+ title = "Dashboard";
+ count = 0;
+}
+```
+
+### Enable Zoneless for New Projects
+
+```typescript
+// main.ts - Zoneless Angular (v20+)
+bootstrapApplication(AppComponent, {
+ providers: [provideZonelessChangeDetection()],
+});
+```
+
+**Benefits:**
+
+- No zone.js patches on async APIs
+- Smaller bundle (~15KB savings)
+- Clean stack traces for debugging
+- Better micro-frontend compatibility
+
+---
+
+## 2. Bundle Optimization (CRITICAL)
+
+### Lazy Load Routes
+
+```typescript
+// CORRECT - Lazy load feature routes
+export const routes: Routes = [
+ {
+ path: "admin",
+ loadChildren: () =>
+ import("./admin/admin.routes").then((m) => m.ADMIN_ROUTES),
+ },
+ {
+ path: "dashboard",
+ loadComponent: () =>
+ import("./dashboard/dashboard.component").then(
+ (m) => m.DashboardComponent,
+ ),
+ },
+];
+
+// WRONG - Eager loading everything
+import { AdminModule } from "./admin/admin.module";
+export const routes: Routes = [
+ { path: "admin", component: AdminComponent }, // In main bundle
+];
+```
+
+### Use @defer for Heavy Components
+
+```html
+
+@defer (on viewport) {
+
+} @placeholder {
+
+}
+
+
+
+```
+
+### Avoid Barrel File Re-exports
+
+```typescript
+// WRONG - Imports entire barrel, breaks tree-shaking
+import { Button, Modal, Table } from "@shared/components";
+
+// CORRECT - Direct imports
+import { Button } from "@shared/components/button/button.component";
+import { Modal } from "@shared/components/modal/modal.component";
+```
+
+### Dynamic Import Third-Party Libraries
+
+```typescript
+// CORRECT - Load heavy library on demand
+async loadChart() {
+ const { Chart } = await import('chart.js');
+ this.chart = new Chart(this.canvas, config);
+}
+
+// WRONG - Bundle Chart.js in main chunk
+import { Chart } from 'chart.js';
+```
+
+---
+
+## 3. Rendering Performance (HIGH)
+
+### Always Use trackBy with @for
+
+```html
+
+@for (item of items(); track item.id) {
+
+}
+
+
+@for (item of items(); track $index) {
+
+}
+```
+
+### Use Virtual Scrolling for Large Lists
+
+```typescript
+import { CdkVirtualScrollViewport, CdkFixedSizeVirtualScroll } from '@angular/cdk/scrolling';
+
+@Component({
+ imports: [CdkVirtualScrollViewport, CdkFixedSizeVirtualScroll],
+ template: `
+
+
+ {{ item.name }}
+
+
+ `
+})
+```
+
+### Prefer Pure Pipes Over Methods
+
+```typescript
+// CORRECT - Pure pipe, memoized
+@Pipe({ name: 'filterActive', standalone: true, pure: true })
+export class FilterActivePipe implements PipeTransform {
+ transform(items: Item[]): Item[] {
+ return items.filter(i => i.active);
+ }
+}
+
+// Template
+@for (item of items() | filterActive; track item.id) { ... }
+
+// WRONG - Method called every change detection
+@for (item of getActiveItems(); track item.id) { ... }
+```
+
+### Use computed() for Derived Data
+
+```typescript
+// CORRECT - Computed, cached until dependencies change
+export class ProductStore {
+ products = signal([]);
+ filter = signal('');
+
+ filteredProducts = computed(() => {
+ const f = this.filter().toLowerCase();
+ return this.products().filter(p =>
+ p.name.toLowerCase().includes(f)
+ );
+ });
+}
+
+// WRONG - Recalculates every access
+get filteredProducts() {
+ return this.products.filter(p =>
+ p.name.toLowerCase().includes(this.filter)
+ );
+}
+```
+
+---
+
+## 4. Server-Side Rendering (HIGH)
+
+### Configure Incremental Hydration
+
+```typescript
+// app.config.ts
+import {
+ provideClientHydration,
+ withIncrementalHydration,
+} from "@angular/platform-browser";
+
+export const appConfig: ApplicationConfig = {
+ providers: [
+ provideClientHydration(withIncrementalHydration(), withEventReplay()),
+ ],
+};
+```
+
+### Defer Non-Critical Content
+
+```html
+
+
+
+
+
+@defer (hydrate on viewport) {
+
+} @defer (hydrate on interaction) {
+
+}
+```
+
+### Use TransferState for SSR Data
+
+```typescript
+@Injectable({ providedIn: "root" })
+export class DataService {
+ private http = inject(HttpClient);
+ private transferState = inject(TransferState);
+ private platformId = inject(PLATFORM_ID);
+
+ getData(key: string): Observable {
+ const stateKey = makeStateKey(key);
+
+ if (isPlatformBrowser(this.platformId)) {
+ const cached = this.transferState.get(stateKey, null);
+ if (cached) {
+ this.transferState.remove(stateKey);
+ return of(cached);
+ }
+ }
+
+ return this.http.get(`/api/${key}`).pipe(
+ tap((data) => {
+ if (isPlatformServer(this.platformId)) {
+ this.transferState.set(stateKey, data);
+ }
+ }),
+ );
+ }
+}
+```
+
+---
+
+## 5. Template Optimization (MEDIUM)
+
+### Use New Control Flow Syntax
+
+```html
+
+@if (user()) {
+{{ user()!.name }}
+} @else {
+Guest
+} @for (item of items(); track item.id) {
+
+} @empty {
+No items
+}
+
+
+{{ user.name }}
+Guest
+```
+
+### Avoid Complex Template Expressions
+
+```typescript
+// CORRECT - Precompute in component
+class Component {
+ items = signal- ([]);
+ sortedItems = computed(() =>
+ [...this.items()].sort((a, b) => a.name.localeCompare(b.name))
+ );
+}
+
+// Template
+@for (item of sortedItems(); track item.id) { ... }
+
+// WRONG - Sorting in template every render
+@for (item of items() | sort:'name'; track item.id) { ... }
+```
+
+---
+
+## 6. State Management (MEDIUM)
+
+### Use Selectors to Prevent Re-renders
+
+```typescript
+// CORRECT - Selective subscription
+@Component({
+ template: `{{ userName() }}`,
+})
+class HeaderComponent {
+ private store = inject(Store);
+ // Only re-renders when userName changes
+ userName = this.store.selectSignal(selectUserName);
+}
+
+// WRONG - Subscribing to entire state
+@Component({
+ template: `{{ state().user.name }}`,
+})
+class HeaderComponent {
+ private store = inject(Store);
+ // Re-renders on ANY state change
+ state = toSignal(this.store);
+}
+```
+
+### Colocate State with Features
+
+```typescript
+// CORRECT - Feature-scoped store
+@Injectable() // NOT providedIn: 'root'
+export class ProductStore { ... }
+
+@Component({
+ providers: [ProductStore], // Scoped to component tree
+})
+export class ProductPageComponent {
+ store = inject(ProductStore);
+}
+
+// WRONG - Everything in global store
+@Injectable({ providedIn: 'root' })
+export class GlobalStore {
+ // Contains ALL app state - hard to tree-shake
+}
+```
+
+---
+
+## 7. Memory Management (LOW-MEDIUM)
+
+### Use takeUntilDestroyed for Subscriptions
+
+```typescript
+import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
+
+@Component({...})
+export class DataComponent {
+ private destroyRef = inject(DestroyRef);
+
+ constructor() {
+ this.data$.pipe(
+ takeUntilDestroyed(this.destroyRef)
+ ).subscribe(data => this.process(data));
+ }
+}
+
+// WRONG - Manual subscription management
+export class DataComponent implements OnDestroy {
+ private subscription!: Subscription;
+
+ ngOnInit() {
+ this.subscription = this.data$.subscribe(...);
+ }
+
+ ngOnDestroy() {
+ this.subscription.unsubscribe(); // Easy to forget
+ }
+}
+```
+
+### Prefer Signals Over Subscriptions
+
+```typescript
+// CORRECT - No subscription needed
+@Component({
+ template: `
{{ data().name }}
`,
+})
+export class Component {
+ data = toSignal(this.service.data$, { initialValue: null });
+}
+
+// WRONG - Manual subscription
+@Component({
+ template: `{{ data?.name }}
`,
+})
+export class Component implements OnInit, OnDestroy {
+ data: Data | null = null;
+ private sub!: Subscription;
+
+ ngOnInit() {
+ this.sub = this.service.data$.subscribe((d) => (this.data = d));
+ }
+
+ ngOnDestroy() {
+ this.sub.unsubscribe();
+ }
+}
+```
+
+---
+
+## Quick Reference Checklist
+
+### New Component
+
+- [ ] `changeDetection: ChangeDetectionStrategy.OnPush`
+- [ ] `standalone: true`
+- [ ] Signals for state (`signal()`, `input()`, `output()`)
+- [ ] `inject()` for dependencies
+- [ ] `@for` with `track` expression
+
+### Performance Review
+
+- [ ] No methods in templates (use pipes or computed)
+- [ ] Large lists virtualized
+- [ ] Heavy components deferred
+- [ ] Routes lazy-loaded
+- [ ] Third-party libs dynamically imported
+
+### SSR Check
+
+- [ ] Hydration configured
+- [ ] Critical content renders first
+- [ ] Non-critical content uses `@defer (hydrate on ...)`
+- [ ] TransferState for server-fetched data
+
+---
+
+## Resources
+
+- [Angular Performance Guide](https://angular.dev/best-practices/performance)
+- [Zoneless Angular](https://angular.dev/guide/experimental/zoneless)
+- [Angular SSR Guide](https://angular.dev/guide/ssr)
+- [Change Detection Deep Dive](https://angular.dev/guide/change-detection)
diff --git a/skills/angular-state-management/SKILL.md b/skills/angular-state-management/SKILL.md
new file mode 100644
index 00000000..d8afaa87
--- /dev/null
+++ b/skills/angular-state-management/SKILL.md
@@ -0,0 +1,632 @@
+---
+name: angular-state-management
+description: Master modern Angular state management with Signals, NgRx, and RxJS. Use when setting up global state, managing component stores, choosing between state solutions, or migrating from legacy patterns.
+---
+
+# Angular State Management
+
+Comprehensive guide to modern Angular state management patterns, from Signal-based local state to global stores and server state synchronization.
+
+## When to Use This Skill
+
+- Setting up global state management in Angular
+- Choosing between Signals, NgRx, or Akita
+- Managing component-level stores
+- Implementing optimistic updates
+- Debugging state-related issues
+- Migrating from legacy state patterns
+
+## Do Not Use This Skill When
+
+- The task is unrelated to Angular state management
+- You need React state management → use `react-state-management`
+
+---
+
+## Core Concepts
+
+### State Categories
+
+| Type | Description | Solutions |
+| ---------------- | ---------------------------- | --------------------- |
+| **Local State** | Component-specific, UI state | Signals, `signal()` |
+| **Shared State** | Between related components | Signal services |
+| **Global State** | App-wide, complex | NgRx, Akita, Elf |
+| **Server State** | Remote data, caching | NgRx Query, RxAngular |
+| **URL State** | Route parameters | ActivatedRoute |
+| **Form State** | Input values, validation | Reactive Forms |
+
+### Selection Criteria
+
+```
+Small app, simple state → Signal Services
+Medium app, moderate state → Component Stores
+Large app, complex state → NgRx Store
+Heavy server interaction → NgRx Query + Signal Services
+Real-time updates → RxAngular + Signals
+```
+
+---
+
+## Quick Start: Signal-Based State
+
+### Pattern 1: Simple Signal Service
+
+```typescript
+// services/counter.service.ts
+import { Injectable, signal, computed } from "@angular/core";
+
+@Injectable({ providedIn: "root" })
+export class CounterService {
+ // Private writable signals
+ private _count = signal(0);
+
+ // Public read-only
+ readonly count = this._count.asReadonly();
+ readonly doubled = computed(() => this._count() * 2);
+ readonly isPositive = computed(() => this._count() > 0);
+
+ increment() {
+ this._count.update((v) => v + 1);
+ }
+
+ decrement() {
+ this._count.update((v) => v - 1);
+ }
+
+ reset() {
+ this._count.set(0);
+ }
+}
+
+// Usage in component
+@Component({
+ template: `
+ Count: {{ counter.count() }}
+ Doubled: {{ counter.doubled() }}
+
+ `,
+})
+export class CounterComponent {
+ counter = inject(CounterService);
+}
+```
+
+### Pattern 2: Feature Signal Store
+
+```typescript
+// stores/user.store.ts
+import { Injectable, signal, computed, inject } from "@angular/core";
+import { HttpClient } from "@angular/common/http";
+import { toSignal } from "@angular/core/rxjs-interop";
+
+interface User {
+ id: string;
+ name: string;
+ email: string;
+}
+
+interface UserState {
+ user: User | null;
+ loading: boolean;
+ error: string | null;
+}
+
+@Injectable({ providedIn: "root" })
+export class UserStore {
+ private http = inject(HttpClient);
+
+ // State signals
+ private _user = signal(null);
+ private _loading = signal(false);
+ private _error = signal(null);
+
+ // Selectors (read-only computed)
+ readonly user = computed(() => this._user());
+ readonly loading = computed(() => this._loading());
+ readonly error = computed(() => this._error());
+ readonly isAuthenticated = computed(() => this._user() !== null);
+ readonly displayName = computed(() => this._user()?.name ?? "Guest");
+
+ // Actions
+ async loadUser(id: string) {
+ this._loading.set(true);
+ this._error.set(null);
+
+ try {
+ const user = await fetch(`/api/users/${id}`).then((r) => r.json());
+ this._user.set(user);
+ } catch (e) {
+ this._error.set("Failed to load user");
+ } finally {
+ this._loading.set(false);
+ }
+ }
+
+ updateUser(updates: Partial) {
+ this._user.update((user) => (user ? { ...user, ...updates } : null));
+ }
+
+ logout() {
+ this._user.set(null);
+ this._error.set(null);
+ }
+}
+```
+
+### Pattern 3: SignalStore (NgRx Signals)
+
+```typescript
+// stores/products.store.ts
+import {
+ signalStore,
+ withState,
+ withMethods,
+ withComputed,
+ patchState,
+} from "@ngrx/signals";
+import { inject } from "@angular/core";
+import { ProductService } from "./product.service";
+
+interface ProductState {
+ products: Product[];
+ loading: boolean;
+ filter: string;
+}
+
+const initialState: ProductState = {
+ products: [],
+ loading: false,
+ filter: "",
+};
+
+export const ProductStore = signalStore(
+ { providedIn: "root" },
+
+ withState(initialState),
+
+ withComputed((store) => ({
+ filteredProducts: computed(() => {
+ const filter = store.filter().toLowerCase();
+ return store
+ .products()
+ .filter((p) => p.name.toLowerCase().includes(filter));
+ }),
+ totalCount: computed(() => store.products().length),
+ })),
+
+ withMethods((store, productService = inject(ProductService)) => ({
+ async loadProducts() {
+ patchState(store, { loading: true });
+
+ try {
+ const products = await productService.getAll();
+ patchState(store, { products, loading: false });
+ } catch {
+ patchState(store, { loading: false });
+ }
+ },
+
+ setFilter(filter: string) {
+ patchState(store, { filter });
+ },
+
+ addProduct(product: Product) {
+ patchState(store, ({ products }) => ({
+ products: [...products, product],
+ }));
+ },
+ })),
+);
+
+// Usage
+@Component({
+ template: `
+
+ @if (store.loading()) {
+
+ } @else {
+ @for (product of store.filteredProducts(); track product.id) {
+
+ }
+ }
+ `,
+})
+export class ProductListComponent {
+ store = inject(ProductStore);
+
+ ngOnInit() {
+ this.store.loadProducts();
+ }
+}
+```
+
+---
+
+## NgRx Store (Global State)
+
+### Setup
+
+```typescript
+// store/app.state.ts
+import { ActionReducerMap } from "@ngrx/store";
+
+export interface AppState {
+ user: UserState;
+ cart: CartState;
+}
+
+export const reducers: ActionReducerMap = {
+ user: userReducer,
+ cart: cartReducer,
+};
+
+// main.ts
+bootstrapApplication(AppComponent, {
+ providers: [
+ provideStore(reducers),
+ provideEffects([UserEffects, CartEffects]),
+ provideStoreDevtools({ maxAge: 25 }),
+ ],
+});
+```
+
+### Feature Slice Pattern
+
+```typescript
+// store/user/user.actions.ts
+import { createActionGroup, props, emptyProps } from "@ngrx/store";
+
+export const UserActions = createActionGroup({
+ source: "User",
+ events: {
+ "Load User": props<{ userId: string }>(),
+ "Load User Success": props<{ user: User }>(),
+ "Load User Failure": props<{ error: string }>(),
+ "Update User": props<{ updates: Partial }>(),
+ Logout: emptyProps(),
+ },
+});
+```
+
+```typescript
+// store/user/user.reducer.ts
+import { createReducer, on } from "@ngrx/store";
+import { UserActions } from "./user.actions";
+
+export interface UserState {
+ user: User | null;
+ loading: boolean;
+ error: string | null;
+}
+
+const initialState: UserState = {
+ user: null,
+ loading: false,
+ error: null,
+};
+
+export const userReducer = createReducer(
+ initialState,
+
+ on(UserActions.loadUser, (state) => ({
+ ...state,
+ loading: true,
+ error: null,
+ })),
+
+ on(UserActions.loadUserSuccess, (state, { user }) => ({
+ ...state,
+ user,
+ loading: false,
+ })),
+
+ on(UserActions.loadUserFailure, (state, { error }) => ({
+ ...state,
+ loading: false,
+ error,
+ })),
+
+ on(UserActions.logout, () => initialState),
+);
+```
+
+```typescript
+// store/user/user.selectors.ts
+import { createFeatureSelector, createSelector } from "@ngrx/store";
+import { UserState } from "./user.reducer";
+
+export const selectUserState = createFeatureSelector("user");
+
+export const selectUser = createSelector(
+ selectUserState,
+ (state) => state.user,
+);
+
+export const selectUserLoading = createSelector(
+ selectUserState,
+ (state) => state.loading,
+);
+
+export const selectIsAuthenticated = createSelector(
+ selectUser,
+ (user) => user !== null,
+);
+```
+
+```typescript
+// store/user/user.effects.ts
+import { Injectable, inject } from "@angular/core";
+import { Actions, createEffect, ofType } from "@ngrx/effects";
+import { switchMap, map, catchError, of } from "rxjs";
+
+@Injectable()
+export class UserEffects {
+ private actions$ = inject(Actions);
+ private userService = inject(UserService);
+
+ loadUser$ = createEffect(() =>
+ this.actions$.pipe(
+ ofType(UserActions.loadUser),
+ switchMap(({ userId }) =>
+ this.userService.getUser(userId).pipe(
+ map((user) => UserActions.loadUserSuccess({ user })),
+ catchError((error) =>
+ of(UserActions.loadUserFailure({ error: error.message })),
+ ),
+ ),
+ ),
+ ),
+ );
+}
+```
+
+### Component Usage
+
+```typescript
+@Component({
+ template: `
+ @if (loading()) {
+
+ } @else if (user(); as user) {
+ Welcome, {{ user.name }}
+
+ }
+ `,
+})
+export class HeaderComponent {
+ private store = inject(Store);
+
+ user = this.store.selectSignal(selectUser);
+ loading = this.store.selectSignal(selectUserLoading);
+
+ logout() {
+ this.store.dispatch(UserActions.logout());
+ }
+}
+```
+
+---
+
+## RxJS-Based Patterns
+
+### Component Store (Local Feature State)
+
+```typescript
+// stores/todo.store.ts
+import { Injectable } from "@angular/core";
+import { ComponentStore } from "@ngrx/component-store";
+import { switchMap, tap, catchError, EMPTY } from "rxjs";
+
+interface TodoState {
+ todos: Todo[];
+ loading: boolean;
+}
+
+@Injectable()
+export class TodoStore extends ComponentStore {
+ constructor(private todoService: TodoService) {
+ super({ todos: [], loading: false });
+ }
+
+ // Selectors
+ readonly todos$ = this.select((state) => state.todos);
+ readonly loading$ = this.select((state) => state.loading);
+ readonly completedCount$ = this.select(
+ this.todos$,
+ (todos) => todos.filter((t) => t.completed).length,
+ );
+
+ // Updaters
+ readonly addTodo = this.updater((state, todo: Todo) => ({
+ ...state,
+ todos: [...state.todos, todo],
+ }));
+
+ readonly toggleTodo = this.updater((state, id: string) => ({
+ ...state,
+ todos: state.todos.map((t) =>
+ t.id === id ? { ...t, completed: !t.completed } : t,
+ ),
+ }));
+
+ // Effects
+ readonly loadTodos = this.effect((trigger$) =>
+ trigger$.pipe(
+ tap(() => this.patchState({ loading: true })),
+ switchMap(() =>
+ this.todoService.getAll().pipe(
+ tap({
+ next: (todos) => this.patchState({ todos, loading: false }),
+ error: () => this.patchState({ loading: false }),
+ }),
+ catchError(() => EMPTY),
+ ),
+ ),
+ ),
+ );
+}
+```
+
+---
+
+## Server State with Signals
+
+### HTTP + Signals Pattern
+
+```typescript
+// services/api.service.ts
+import { Injectable, signal, inject } from "@angular/core";
+import { HttpClient } from "@angular/common/http";
+import { toSignal } from "@angular/core/rxjs-interop";
+
+interface ApiState {
+ data: T | null;
+ loading: boolean;
+ error: string | null;
+}
+
+@Injectable({ providedIn: "root" })
+export class ProductApiService {
+ private http = inject(HttpClient);
+
+ private _state = signal>({
+ data: null,
+ loading: false,
+ error: null,
+ });
+
+ readonly products = computed(() => this._state().data ?? []);
+ readonly loading = computed(() => this._state().loading);
+ readonly error = computed(() => this._state().error);
+
+ async fetchProducts(): Promise {
+ this._state.update((s) => ({ ...s, loading: true, error: null }));
+
+ try {
+ const data = await firstValueFrom(
+ this.http.get("/api/products"),
+ );
+ this._state.update((s) => ({ ...s, data, loading: false }));
+ } catch (e) {
+ this._state.update((s) => ({
+ ...s,
+ loading: false,
+ error: "Failed to fetch products",
+ }));
+ }
+ }
+
+ // Optimistic update
+ async deleteProduct(id: string): Promise {
+ const previousData = this._state().data;
+
+ // Optimistically remove
+ this._state.update((s) => ({
+ ...s,
+ data: s.data?.filter((p) => p.id !== id) ?? null,
+ }));
+
+ try {
+ await firstValueFrom(this.http.delete(`/api/products/${id}`));
+ } catch {
+ // Rollback on error
+ this._state.update((s) => ({ ...s, data: previousData }));
+ }
+ }
+}
+```
+
+---
+
+## Best Practices
+
+### Do's
+
+| Practice | Why |
+| ---------------------------------- | ---------------------------------- |
+| Use Signals for local state | Simple, reactive, no subscriptions |
+| Use `computed()` for derived data | Auto-updates, memoized |
+| Colocate state with feature | Easier to maintain |
+| Use NgRx for complex flows | Actions, effects, devtools |
+| Prefer `inject()` over constructor | Cleaner, works in factories |
+
+### Don'ts
+
+| Anti-Pattern | Instead |
+| --------------------------------- | ----------------------------------------------------- |
+| Store derived data | Use `computed()` |
+| Mutate signals directly | Use `set()` or `update()` |
+| Over-globalize state | Keep local when possible |
+| Mix RxJS and Signals chaotically | Choose primary, bridge with `toSignal`/`toObservable` |
+| Subscribe in components for state | Use template with signals |
+
+---
+
+## Migration Path
+
+### From BehaviorSubject to Signals
+
+```typescript
+// Before: RxJS-based
+@Injectable({ providedIn: "root" })
+export class OldUserService {
+ private userSubject = new BehaviorSubject(null);
+ user$ = this.userSubject.asObservable();
+
+ setUser(user: User) {
+ this.userSubject.next(user);
+ }
+}
+
+// After: Signal-based
+@Injectable({ providedIn: "root" })
+export class UserService {
+ private _user = signal(null);
+ readonly user = this._user.asReadonly();
+
+ setUser(user: User) {
+ this._user.set(user);
+ }
+}
+```
+
+### Bridging Signals and RxJS
+
+```typescript
+import { toSignal, toObservable } from '@angular/core/rxjs-interop';
+
+// Observable → Signal
+@Component({...})
+export class ExampleComponent {
+ private route = inject(ActivatedRoute);
+
+ // Convert Observable to Signal
+ userId = toSignal(
+ this.route.params.pipe(map(p => p['id'])),
+ { initialValue: '' }
+ );
+}
+
+// Signal → Observable
+export class DataService {
+ private filter = signal('');
+
+ // Convert Signal to Observable
+ filter$ = toObservable(this.filter);
+
+ filteredData$ = this.filter$.pipe(
+ debounceTime(300),
+ switchMap(filter => this.http.get(`/api/data?q=${filter}`))
+ );
+}
+```
+
+---
+
+## Resources
+
+- [Angular Signals Guide](https://angular.dev/guide/signals)
+- [NgRx Documentation](https://ngrx.io/)
+- [NgRx SignalStore](https://ngrx.io/guide/signals)
+- [RxAngular](https://www.rx-angular.io/)
diff --git a/skills/angular-ui-patterns/SKILL.md b/skills/angular-ui-patterns/SKILL.md
new file mode 100644
index 00000000..cc9601cd
--- /dev/null
+++ b/skills/angular-ui-patterns/SKILL.md
@@ -0,0 +1,506 @@
+---
+name: angular-ui-patterns
+description: Modern Angular UI patterns for loading states, error handling, and data display. Use when building UI components, handling async data, or managing component states.
+---
+
+# Angular UI Patterns
+
+## Core Principles
+
+1. **Never show stale UI** - Loading states only when actually loading
+2. **Always surface errors** - Users must know when something fails
+3. **Optimistic updates** - Make the UI feel instant
+4. **Progressive disclosure** - Use `@defer` to show content as available
+5. **Graceful degradation** - Partial data is better than no data
+
+---
+
+## Loading State Patterns
+
+### The Golden Rule
+
+**Show loading indicator ONLY when there's no data to display.**
+
+```typescript
+@Component({
+ template: `
+ @if (error()) {
+
+ } @else if (loading() && !items().length) {
+
+ } @else if (!items().length) {
+
+ } @else {
+
+ }
+ `,
+})
+export class ItemListComponent {
+ private store = inject(ItemStore);
+
+ items = this.store.items;
+ loading = this.store.loading;
+ error = this.store.error;
+}
+```
+
+### Loading State Decision Tree
+
+```
+Is there an error?
+ → Yes: Show error state with retry option
+ → No: Continue
+
+Is it loading AND we have no data?
+ → Yes: Show loading indicator (spinner/skeleton)
+ → No: Continue
+
+Do we have data?
+ → Yes, with items: Show the data
+ → Yes, but empty: Show empty state
+ → No: Show loading (fallback)
+```
+
+### Skeleton vs Spinner
+
+| Use Skeleton When | Use Spinner When |
+| -------------------- | --------------------- |
+| Known content shape | Unknown content shape |
+| List/card layouts | Modal actions |
+| Initial page load | Button submissions |
+| Content placeholders | Inline operations |
+
+---
+
+## Control Flow Patterns
+
+### @if/@else for Conditional Rendering
+
+```html
+@if (user(); as user) {
+Welcome, {{ user.name }}
+} @else if (loading()) {
+
+} @else {
+Sign In
+}
+```
+
+### @for with Track
+
+```html
+@for (item of items(); track item.id) {
+
+} @empty {
+
+}
+```
+
+### @defer for Progressive Loading
+
+```html
+
+
+
+
+
+@defer (on viewport) {
+
+} @placeholder {
+
+} @loading (minimum 200ms) {
+
+} @error {
+
+}
+```
+
+---
+
+## Error Handling Patterns
+
+### Error Handling Hierarchy
+
+```
+1. Inline error (field-level) → Form validation errors
+2. Toast notification → Recoverable errors, user can retry
+3. Error banner → Page-level errors, data still partially usable
+4. Full error screen → Unrecoverable, needs user action
+```
+
+### Always Show Errors
+
+**CRITICAL: Never swallow errors silently.**
+
+```typescript
+// CORRECT - Error always surfaced to user
+@Component({...})
+export class CreateItemComponent {
+ private store = inject(ItemStore);
+ private toast = inject(ToastService);
+
+ async create(data: CreateItemDto) {
+ try {
+ await this.store.create(data);
+ this.toast.success('Item created successfully');
+ this.router.navigate(['/items']);
+ } catch (error) {
+ console.error('createItem failed:', error);
+ this.toast.error('Failed to create item. Please try again.');
+ }
+ }
+}
+
+// WRONG - Error silently caught
+async create(data: CreateItemDto) {
+ try {
+ await this.store.create(data);
+ } catch (error) {
+ console.error(error); // User sees nothing!
+ }
+}
+```
+
+### Error State Component Pattern
+
+```typescript
+@Component({
+ selector: "app-error-state",
+ standalone: true,
+ imports: [NgOptimizedImage],
+ template: `
+
+
![]()
+
{{ title() }}
+
{{ message() }}
+ @if (retry.observed) {
+
+ }
+
+ `,
+})
+export class ErrorStateComponent {
+ title = input("Something went wrong");
+ message = input("An unexpected error occurred");
+ retry = output();
+}
+```
+
+---
+
+## Button State Patterns
+
+### Button Loading State
+
+```html
+
+```
+
+### Disable During Operations
+
+**CRITICAL: Always disable triggers during async operations.**
+
+```typescript
+// CORRECT - Button disabled while loading
+@Component({
+ template: `
+
+ `
+})
+export class SaveButtonComponent {
+ saving = signal(false);
+
+ async save() {
+ this.saving.set(true);
+ try {
+ await this.service.save();
+ } finally {
+ this.saving.set(false);
+ }
+ }
+}
+
+// WRONG - User can click multiple times
+
+```
+
+---
+
+## Empty States
+
+### Empty State Requirements
+
+Every list/collection MUST have an empty state:
+
+```html
+@for (item of items(); track item.id) {
+
+} @empty {
+
+}
+```
+
+### Contextual Empty States
+
+```typescript
+@Component({
+ selector: "app-empty-state",
+ template: `
+
+
+
{{ title() }}
+
{{ description() }}
+ @if (actionLabel()) {
+
+ }
+
+ `,
+})
+export class EmptyStateComponent {
+ icon = input("inbox");
+ title = input.required();
+ description = input("");
+ actionLabel = input(null);
+ action = output();
+}
+```
+
+---
+
+## Form Patterns
+
+### Form with Loading and Validation
+
+```typescript
+@Component({
+ template: `
+
+ `,
+})
+export class UserFormComponent {
+ private fb = inject(FormBuilder);
+
+ submitting = signal(false);
+
+ form = this.fb.group({
+ name: ["", [Validators.required, Validators.minLength(2)]],
+ email: ["", [Validators.required, Validators.email]],
+ });
+
+ isFieldInvalid(field: string): boolean {
+ const control = this.form.get(field);
+ return control ? control.invalid && control.touched : false;
+ }
+
+ getFieldError(field: string): string {
+ const control = this.form.get(field);
+ if (control?.hasError("required")) return "This field is required";
+ if (control?.hasError("email")) return "Invalid email format";
+ if (control?.hasError("minlength")) return "Too short";
+ return "";
+ }
+
+ async onSubmit() {
+ if (this.form.invalid) return;
+
+ this.submitting.set(true);
+ try {
+ await this.service.submit(this.form.value);
+ this.toast.success("Submitted successfully");
+ } catch {
+ this.toast.error("Submission failed");
+ } finally {
+ this.submitting.set(false);
+ }
+ }
+}
+```
+
+---
+
+## Dialog/Modal Patterns
+
+### Confirmation Dialog
+
+```typescript
+// dialog.service.ts
+@Injectable({ providedIn: 'root' })
+export class DialogService {
+ private dialog = inject(Dialog); // CDK Dialog or custom
+
+ async confirm(options: {
+ title: string;
+ message: string;
+ confirmText?: string;
+ cancelText?: string;
+ }): Promise {
+ const dialogRef = this.dialog.open(ConfirmDialogComponent, {
+ data: options,
+ });
+
+ return await firstValueFrom(dialogRef.closed) ?? false;
+ }
+}
+
+// Usage
+async deleteItem(item: Item) {
+ const confirmed = await this.dialog.confirm({
+ title: 'Delete Item',
+ message: `Are you sure you want to delete "${item.name}"?`,
+ confirmText: 'Delete',
+ });
+
+ if (confirmed) {
+ await this.store.delete(item.id);
+ }
+}
+```
+
+---
+
+## Anti-Patterns
+
+### Loading States
+
+```typescript
+// WRONG - Spinner when data exists (causes flash on refetch)
+@if (loading()) {
+
+}
+
+// CORRECT - Only show loading without data
+@if (loading() && !items().length) {
+
+}
+```
+
+### Error Handling
+
+```typescript
+// WRONG - Error swallowed
+try {
+ await this.service.save();
+} catch (e) {
+ console.log(e); // User has no idea!
+}
+
+// CORRECT - Error surfaced
+try {
+ await this.service.save();
+} catch (e) {
+ console.error("Save failed:", e);
+ this.toast.error("Failed to save. Please try again.");
+}
+```
+
+### Button States
+
+```html
+
+
+
+
+
+```
+
+---
+
+## UI State Checklist
+
+Before completing any UI component:
+
+### UI States
+
+- [ ] Error state handled and shown to user
+- [ ] Loading state shown only when no data exists
+- [ ] Empty state provided for collections (`@empty` block)
+- [ ] Buttons disabled during async operations
+- [ ] Buttons show loading indicator when appropriate
+
+### Data & Mutations
+
+- [ ] All async operations have error handling
+- [ ] All user actions have feedback (toast/visual)
+- [ ] Optimistic updates rollback on failure
+
+### Accessibility
+
+- [ ] Loading states announced to screen readers
+- [ ] Error messages linked to form fields
+- [ ] Focus management after state changes
+
+---
+
+## Integration with Other Skills
+
+- **angular-state-management**: Use Signal stores for state
+- **angular**: Apply modern patterns (Signals, @defer)
+- **testing-patterns**: Test all UI states
diff --git a/skills/angular/SKILL.md b/skills/angular/SKILL.md
new file mode 100644
index 00000000..fa377d6a
--- /dev/null
+++ b/skills/angular/SKILL.md
@@ -0,0 +1,763 @@
+---
+name: angular
+description: >-
+ Modern Angular (v20+) expert with deep knowledge of Signals, Standalone
+ Components, Zoneless applications, SSR/Hydration, and reactive patterns.
+ Use PROACTIVELY for Angular development, component architecture, state
+ management, performance optimization, and migration to modern patterns.
+---
+
+# Angular Expert
+
+Master modern Angular development with Signals, Standalone Components, Zoneless applications, SSR/Hydration, and the latest reactive patterns.
+
+## When to Use This Skill
+
+- Building new Angular applications (v20+)
+- Implementing Signals-based reactive patterns
+- Creating Standalone Components and migrating from NgModules
+- Configuring Zoneless Angular applications
+- Implementing SSR, prerendering, and hydration
+- Optimizing Angular performance
+- Adopting modern Angular patterns and best practices
+
+## Do Not Use This Skill When
+
+- Migrating from AngularJS (1.x) → use `angular-migration` skill
+- Working with legacy Angular apps that cannot upgrade
+- General TypeScript issues → use `typescript-expert` skill
+
+## Instructions
+
+1. Assess the Angular version and project structure
+2. Apply modern patterns (Signals, Standalone, Zoneless)
+3. Implement with proper typing and reactivity
+4. Validate with build and tests
+
+## Safety
+
+- Always test changes in development before production
+- Gradual migration for existing apps (don't big-bang refactor)
+- Keep backward compatibility during transitions
+
+---
+
+## Angular Version Timeline
+
+| Version | Release | Key Features |
+| -------------- | ------- | ------------------------------------------------------ |
+| **Angular 20** | Q2 2025 | Signals stable, Zoneless stable, Incremental hydration |
+| **Angular 21** | Q4 2025 | Signals-first default, Enhanced SSR |
+| **Angular 22** | Q2 2026 | Signal Forms, Selectorless components |
+
+---
+
+## 1. Signals: The New Reactive Primitive
+
+Signals are Angular's fine-grained reactivity system, replacing zone.js-based change detection.
+
+### Core Concepts
+
+```typescript
+import { signal, computed, effect } from "@angular/core";
+
+// Writable signal
+const count = signal(0);
+
+// Read value
+console.log(count()); // 0
+
+// Update value
+count.set(5); // Direct set
+count.update((v) => v + 1); // Functional update
+
+// Computed (derived) signal
+const doubled = computed(() => count() * 2);
+
+// Effect (side effects)
+effect(() => {
+ console.log(`Count changed to: ${count()}`);
+});
+```
+
+### Signal-Based Inputs and Outputs
+
+```typescript
+import { Component, input, output, model } from "@angular/core";
+
+@Component({
+ selector: "app-user-card",
+ standalone: true,
+ template: `
+
+
{{ name() }}
+ {{ role() }}
+
+
+ `,
+})
+export class UserCardComponent {
+ // Signal inputs (read-only)
+ id = input.required();
+ name = input.required();
+ role = input("User"); // With default
+
+ // Output
+ select = output();
+
+ // Two-way binding (model)
+ isSelected = model(false);
+}
+
+// Usage:
+//
+```
+
+### Signal Queries (ViewChild/ContentChild)
+
+```typescript
+import {
+ Component,
+ viewChild,
+ viewChildren,
+ contentChild,
+} from "@angular/core";
+
+@Component({
+ selector: "app-container",
+ standalone: true,
+ template: `
+
+
+ `,
+})
+export class ContainerComponent {
+ // Signal-based queries
+ searchInput = viewChild("searchInput");
+ items = viewChildren(ItemComponent);
+ projectedContent = contentChild(HeaderDirective);
+
+ focusSearch() {
+ this.searchInput()?.nativeElement.focus();
+ }
+}
+```
+
+### When to Use Signals vs RxJS
+
+| Use Case | Signals | RxJS |
+| ----------------------- | --------------- | -------------------------------- |
+| Local component state | ✅ Preferred | Overkill |
+| Derived/computed values | ✅ `computed()` | `combineLatest` works |
+| Side effects | ✅ `effect()` | `tap` operator |
+| HTTP requests | ❌ | ✅ HttpClient returns Observable |
+| Event streams | ❌ | ✅ `fromEvent`, operators |
+| Complex async flows | ❌ | ✅ `switchMap`, `mergeMap` |
+
+---
+
+## 2. Standalone Components
+
+Standalone components are self-contained and don't require NgModule declarations.
+
+### Creating Standalone Components
+
+```typescript
+import { Component } from "@angular/core";
+import { CommonModule } from "@angular/common";
+import { RouterLink } from "@angular/router";
+
+@Component({
+ selector: "app-header",
+ standalone: true,
+ imports: [CommonModule, RouterLink], // Direct imports
+ template: `
+
+ `,
+})
+export class HeaderComponent {}
+```
+
+### Bootstrapping Without NgModule
+
+```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()],
+});
+```
+
+### Lazy Loading Standalone Components
+
+```typescript
+// app.routes.ts
+import { Routes } from "@angular/router";
+
+export const routes: Routes = [
+ {
+ path: "dashboard",
+ loadComponent: () =>
+ import("./dashboard/dashboard.component").then(
+ (m) => m.DashboardComponent,
+ ),
+ },
+ {
+ path: "admin",
+ loadChildren: () =>
+ import("./admin/admin.routes").then((m) => m.ADMIN_ROUTES),
+ },
+];
+```
+
+---
+
+## 3. Zoneless Angular
+
+Zoneless applications don't use zone.js, improving performance and debugging.
+
+### Enabling Zoneless Mode
+
+```typescript
+// main.ts
+import { bootstrapApplication } from "@angular/platform-browser";
+import { provideZonelessChangeDetection } from "@angular/core";
+import { AppComponent } from "./app/app.component";
+
+bootstrapApplication(AppComponent, {
+ providers: [provideZonelessChangeDetection()],
+});
+```
+
+### Zoneless Component Patterns
+
+```typescript
+import { Component, signal, ChangeDetectionStrategy } from "@angular/core";
+
+@Component({
+ selector: "app-counter",
+ standalone: true,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ template: `
+ Count: {{ count() }}
+
+ `,
+})
+export class CounterComponent {
+ count = signal(0);
+
+ increment() {
+ this.count.update((v) => v + 1);
+ // No zone.js needed - Signal triggers change detection
+ }
+}
+```
+
+### Key Zoneless Benefits
+
+- **Performance**: No zone.js patches on async APIs
+- **Debugging**: Clean stack traces without zone wrappers
+- **Bundle size**: Smaller without zone.js (~15KB savings)
+- **Interoperability**: Better with Web Components and micro-frontends
+
+---
+
+## 4. Server-Side Rendering & Hydration
+
+### SSR Setup with Angular CLI
+
+```bash
+ng add @angular/ssr
+```
+
+### Hydration Configuration
+
+```typescript
+// app.config.ts
+import { ApplicationConfig } from "@angular/core";
+import {
+ provideClientHydration,
+ withEventReplay,
+} from "@angular/platform-browser";
+
+export const appConfig: ApplicationConfig = {
+ providers: [provideClientHydration(withEventReplay())],
+};
+```
+
+### Incremental Hydration (v20+)
+
+```typescript
+import { Component } from "@angular/core";
+
+@Component({
+ selector: "app-page",
+ standalone: true,
+ template: `
+
+
+ @defer (hydrate on viewport) {
+
+ }
+
+ @defer (hydrate on interaction) {
+
+ }
+ `,
+})
+export class PageComponent {}
+```
+
+### Hydration Triggers
+
+| Trigger | When to Use |
+| ---------------- | --------------------------------------- |
+| `on idle` | Low-priority, hydrate when browser idle |
+| `on viewport` | Hydrate when element enters viewport |
+| `on interaction` | Hydrate on first user interaction |
+| `on hover` | Hydrate when user hovers |
+| `on timer(ms)` | Hydrate after specified delay |
+
+---
+
+## 5. Modern Routing Patterns
+
+### Functional Route Guards
+
+```typescript
+// auth.guard.ts
+import { inject } from "@angular/core";
+import { Router, CanActivateFn } from "@angular/router";
+import { AuthService } from "./auth.service";
+
+export const authGuard: CanActivateFn = (route, state) => {
+ const auth = inject(AuthService);
+ const router = inject(Router);
+
+ if (auth.isAuthenticated()) {
+ return true;
+ }
+
+ return router.createUrlTree(["/login"], {
+ queryParams: { returnUrl: state.url },
+ });
+};
+
+// Usage in routes
+export const routes: Routes = [
+ {
+ path: "dashboard",
+ loadComponent: () => import("./dashboard.component"),
+ canActivate: [authGuard],
+ },
+];
+```
+
+### Route-Level Data Resolvers
+
+```typescript
+import { inject } from '@angular/core';
+import { ResolveFn } from '@angular/router';
+import { UserService } from './user.service';
+import { User } from './user.model';
+
+export const userResolver: ResolveFn = (route) => {
+ const userService = inject(UserService);
+ return userService.getUser(route.paramMap.get('id')!);
+};
+
+// In routes
+{
+ path: 'user/:id',
+ loadComponent: () => import('./user.component'),
+ resolve: { user: userResolver }
+}
+
+// In component
+export class UserComponent {
+ private route = inject(ActivatedRoute);
+ user = toSignal(this.route.data.pipe(map(d => d['user'])));
+}
+```
+
+---
+
+## 6. Dependency Injection Patterns
+
+### Modern inject() Function
+
+```typescript
+import { Component, inject } from '@angular/core';
+import { HttpClient } from '@angular/common/http';
+import { UserService } from './user.service';
+
+@Component({...})
+export class UserComponent {
+ // Modern inject() - no constructor needed
+ private http = inject(HttpClient);
+ private userService = inject(UserService);
+
+ // Works in any injection context
+ users = toSignal(this.userService.getUsers());
+}
+```
+
+### Injection Tokens for Configuration
+
+```typescript
+import { InjectionToken, inject } from "@angular/core";
+
+// Define token
+export const API_BASE_URL = new InjectionToken("API_BASE_URL");
+
+// Provide in config
+bootstrapApplication(AppComponent, {
+ providers: [{ provide: API_BASE_URL, useValue: "https://api.example.com" }],
+});
+
+// Inject in service
+@Injectable({ providedIn: "root" })
+export class ApiService {
+ private baseUrl = inject(API_BASE_URL);
+
+ get(endpoint: string) {
+ return this.http.get(`${this.baseUrl}/${endpoint}`);
+ }
+}
+```
+
+---
+
+## 7. State Management Patterns
+
+### Signal-Based State Service
+
+```typescript
+import { Injectable, signal, computed } from "@angular/core";
+
+interface AppState {
+ user: User | null;
+ theme: "light" | "dark";
+ notifications: Notification[];
+}
+
+@Injectable({ providedIn: "root" })
+export class StateService {
+ // Private writable signals
+ private _user = signal(null);
+ private _theme = signal<"light" | "dark">("light");
+ private _notifications = signal([]);
+
+ // Public read-only computed
+ readonly user = computed(() => this._user());
+ readonly theme = computed(() => this._theme());
+ readonly notifications = computed(() => this._notifications());
+ readonly unreadCount = computed(
+ () => this._notifications().filter((n) => !n.read).length,
+ );
+
+ // Actions
+ setUser(user: User | null) {
+ this._user.set(user);
+ }
+
+ toggleTheme() {
+ this._theme.update((t) => (t === "light" ? "dark" : "light"));
+ }
+
+ addNotification(notification: Notification) {
+ this._notifications.update((n) => [...n, notification]);
+ }
+}
+```
+
+### Component Store Pattern with Signals
+
+```typescript
+import { Injectable, signal, computed, inject } from "@angular/core";
+import { HttpClient } from "@angular/common/http";
+import { toSignal } from "@angular/core/rxjs-interop";
+
+@Injectable()
+export class ProductStore {
+ private http = inject(HttpClient);
+
+ // State
+ private _products = signal([]);
+ private _loading = signal(false);
+ private _filter = signal("");
+
+ // Selectors
+ readonly products = computed(() => this._products());
+ readonly loading = computed(() => this._loading());
+ readonly filteredProducts = computed(() => {
+ const filter = this._filter().toLowerCase();
+ return this._products().filter((p) =>
+ p.name.toLowerCase().includes(filter),
+ );
+ });
+
+ // Actions
+ loadProducts() {
+ this._loading.set(true);
+ this.http.get("/api/products").subscribe({
+ next: (products) => {
+ this._products.set(products);
+ this._loading.set(false);
+ },
+ error: () => this._loading.set(false),
+ });
+ }
+
+ setFilter(filter: string) {
+ this._filter.set(filter);
+ }
+}
+```
+
+---
+
+## 8. Forms with Signals (Coming in v22+)
+
+### Current Reactive Forms
+
+```typescript
+import { Component, inject } from "@angular/core";
+import { FormBuilder, Validators, ReactiveFormsModule } from "@angular/forms";
+
+@Component({
+ selector: "app-user-form",
+ standalone: true,
+ imports: [ReactiveFormsModule],
+ template: `
+
+ `,
+})
+export class UserFormComponent {
+ private fb = inject(FormBuilder);
+
+ form = this.fb.group({
+ name: ["", Validators.required],
+ email: ["", [Validators.required, Validators.email]],
+ });
+
+ onSubmit() {
+ if (this.form.valid) {
+ console.log(this.form.value);
+ }
+ }
+}
+```
+
+### Signal-Aware Form Patterns (Preview)
+
+```typescript
+// Future Signal Forms API (experimental)
+import { Component, signal } from '@angular/core';
+
+@Component({...})
+export class SignalFormComponent {
+ name = signal('');
+ email = signal('');
+
+ // Computed validation
+ isValid = computed(() =>
+ this.name().length > 0 &&
+ this.email().includes('@')
+ );
+
+ submit() {
+ if (this.isValid()) {
+ console.log({ name: this.name(), email: this.email() });
+ }
+ }
+}
+```
+
+---
+
+## 9. Performance Optimization
+
+### Change Detection Strategies
+
+```typescript
+@Component({
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ // Only checks when:
+ // 1. Input signal/reference changes
+ // 2. Event handler runs
+ // 3. Async pipe emits
+ // 4. Signal value changes
+})
+```
+
+### Defer Blocks for Lazy Loading
+
+```typescript
+@Component({
+ template: `
+
+
+
+
+ @defer (on viewport) {
+
+ } @placeholder {
+
+ } @loading (minimum 200ms) {
+
+ } @error {
+ Failed to load chart
+ }
+ `
+})
+```
+
+### NgOptimizedImage
+
+```typescript
+import { NgOptimizedImage } from '@angular/common';
+
+@Component({
+ imports: [NgOptimizedImage],
+ template: `
+
+
+
+ `
+})
+```
+
+---
+
+## 10. Testing Modern Angular
+
+### Testing Signal Components
+
+```typescript
+import { ComponentFixture, TestBed } from "@angular/core/testing";
+import { CounterComponent } from "./counter.component";
+
+describe("CounterComponent", () => {
+ let component: CounterComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [CounterComponent], // Standalone import
+ }).compileComponents();
+
+ fixture = TestBed.createComponent(CounterComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it("should increment count", () => {
+ expect(component.count()).toBe(0);
+
+ component.increment();
+
+ expect(component.count()).toBe(1);
+ });
+
+ it("should update DOM on signal change", () => {
+ component.count.set(5);
+ fixture.detectChanges();
+
+ const el = fixture.nativeElement.querySelector(".count");
+ expect(el.textContent).toContain("5");
+ });
+});
+```
+
+### Testing with Signal Inputs
+
+```typescript
+import { ComponentFixture, TestBed } from "@angular/core/testing";
+import { ComponentRef } from "@angular/core";
+import { UserCardComponent } from "./user-card.component";
+
+describe("UserCardComponent", () => {
+ let fixture: ComponentFixture;
+ let componentRef: ComponentRef;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [UserCardComponent],
+ }).compileComponents();
+
+ fixture = TestBed.createComponent(UserCardComponent);
+ componentRef = fixture.componentRef;
+
+ // Set signal inputs via setInput
+ componentRef.setInput("id", "123");
+ componentRef.setInput("name", "John Doe");
+
+ fixture.detectChanges();
+ });
+
+ it("should display user name", () => {
+ const el = fixture.nativeElement.querySelector("h3");
+ expect(el.textContent).toContain("John Doe");
+ });
+});
+```
+
+---
+
+## Best Practices Summary
+
+| Pattern | ✅ Do | ❌ Don't |
+| -------------------- | ------------------------------ | ------------------------------- |
+| **State** | Use Signals for local state | Overuse RxJS for simple state |
+| **Components** | Standalone with direct imports | Bloated SharedModules |
+| **Change Detection** | OnPush + Signals | Default CD everywhere |
+| **Lazy Loading** | `@defer` and `loadComponent` | Eager load everything |
+| **DI** | `inject()` function | Constructor injection (verbose) |
+| **Inputs** | `input()` signal function | `@Input()` decorator (legacy) |
+| **Zoneless** | Enable for new projects | Force on legacy without testing |
+
+---
+
+## Resources
+
+- [Angular.dev Documentation](https://angular.dev)
+- [Angular Signals Guide](https://angular.dev/guide/signals)
+- [Angular SSR Guide](https://angular.dev/guide/ssr)
+- [Angular Update Guide](https://angular.dev/update-guide)
+- [Angular Blog](https://blog.angular.dev)
+
+---
+
+## Common Troubleshooting
+
+| Issue | Solution |
+| ------------------------------ | --------------------------------------------------- |
+| Signal not updating UI | Ensure `OnPush` + call signal as function `count()` |
+| Hydration mismatch | Check server/client content consistency |
+| Circular dependency | Use `inject()` with `forwardRef` |
+| Zoneless not detecting changes | Trigger via signal updates, not mutations |
+| SSR fetch fails | Use `TransferState` or `withFetch()` |