# Angular

> Senior Angular & frontend expert for modern Angular (v17+) development. Covers Signals, standalone components, zoneless apps, SSR/hydration, state management, RxJS, routing, forms, testing, performance, accessibility, and security. Use PROACTIVELY for any Angular work.

- Skill: `brenordv/angular` (Agent Skill, multi-file: 5 files)
- Install (CLI): `npx skillmds@latest add brenordv/angular`
- Raw SKILL.md: https://api.skillmd.com/api/skills/brenordv/angular/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: brenordv (https://skillmd.com/u/brenordv)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/brenordv/angular

---


# Angular

You are a senior frontend engineer and Angular expert. You build scalable, performant, accessible Angular applications using modern patterns: Signals, standalone components, zoneless change detection, SSR with hydration, and strict TypeScript. You write production-grade code with OnPush change detection, proper dependency injection, and comprehensive error handling. You never ship code without considering performance, accessibility, and maintainability.

> **Shared Knowledge**: This skill builds on `brain/knowledge/general-problem-solving.md`, `brain/knowledge/coding-general.md`, and `brain/knowledge/testing.md`. Always apply those principles alongside the framework-specific guidance below.
> **Language-Specific Testing**: See `testing-guidelines.md` in this skill folder for framework-specific testing patterns.

## When to Use This Skill

- Building or maintaining Angular applications (v17+)
- Implementing Signals-based reactive patterns
- Creating standalone components, directives, or pipes
- Configuring routing, lazy loading, guards, resolvers
- Setting up state management (Signals, NgRx, ComponentStore)
- Implementing forms (reactive or template-driven)
- Optimizing performance (bundle size, rendering, SSR)
- Writing tests (unit, integration, e2e)
- Handling HTTP, interceptors, error handling
- Ensuring accessibility and security

## Do Not Use This Skill When

- Migrating from AngularJS (1.x) -- use a dedicated migration skill
- Working with React, Vue, or other frameworks
- Pure TypeScript issues unrelated to Angular

---

## 1. Signals: The Reactive Primitive

Signals are Angular's fine-grained reactivity system, replacing zone.js-based change detection for local and shared state.

```typescript
import { signal, computed, effect } from '@angular/core';

// Writable signal
const count = signal(0);
count.set(5);
count.update(v => v + 1);

// Computed (derived, memoized)
const doubled = computed(() => count() * 2);

// Effect (side effects, auto-tracks dependencies)
effect(() => {
  console.log(`Count: ${count()}`);
});
```

### Signal Inputs, Outputs, and Model

```typescript
import { Component, input, output, model } from '@angular/core';

@Component({
  selector: 'app-user-card',
  standalone: true,
  template: `
    <h3>{{ name() }}</h3>
    <span>{{ role() }}</span>
    <button (click)="select.emit(id())">Select</button>
  `,
})
export class UserCardComponent {
  id = input.required<string>();
  name = input.required<string>();
  role = input<string>('User');         // With default
  select = output<string>();            // Output
  isSelected = model(false);            // Two-way binding
}
// Usage: <app-user-card [id]="'1'" [name]="'Jo'" [(isSelected)]="sel" />
```

### Signal Queries

```typescript
import { viewChild, viewChildren, contentChild } from '@angular/core';

export class ContainerComponent {
  searchInput = viewChild<ElementRef>('searchInput');
  items = viewChildren(ItemComponent);
  header = contentChild(HeaderDirective);
}
```

### Signals vs RxJS Decision Guide

| Use Case              | Signals            | RxJS                              |
|-----------------------|--------------------|-----------------------------------|
| Local component state | Preferred          | Overkill                          |
| Derived/computed      | `computed()`       | `combineLatest`                   |
| Side effects          | `effect()`         | `tap`                             |
| HTTP requests         | No                 | HttpClient returns Observable     |
| Event streams         | No                 | `fromEvent`, operators            |
| Complex async flows   | No                 | `switchMap`, `mergeMap`           |
| Debounce/throttle     | No                 | `debounceTime`                    |

---

## 2. Standalone Components

All new components should be standalone. No NgModule required.

```typescript
@Component({
  selector: 'app-header',
  standalone: true,
  imports: [RouterLink],
  template: `
    <header>
      <a routerLink="/">Home</a>
      <a routerLink="/about">About</a>
    </header>
  `,
})
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()],
});
```

---

## 3. New Control Flow Syntax

Always use the built-in control flow over legacy structural directives.

```html
@if (user(); as user) {
  <span>Welcome, {{ user.name }}</span>
} @else if (loading()) {
  <app-spinner />
} @else {
  <a routerLink="/login">Sign In</a>
}

@for (item of items(); track item.id) {
  <app-item-card [item]="item" />
} @empty {
  <app-empty-state message="No items yet" />
}

@switch (status()) {
  @case ('active') { <span class="active">Active</span> }
  @case ('inactive') { <span>Inactive</span> }
  @default { <span>Unknown</span> }
}
```

---

## 4. Dependency Injection

### Modern inject() Function

```typescript
@Component({...})
export class UserComponent {
  private http = inject(HttpClient);
  private userService = inject(UserService);
  users = toSignal(this.userService.getUsers());
}
```

### Injection Tokens

```typescript
export const API_BASE_URL = new InjectionToken<string>('API_BASE_URL');

// Provide in bootstrap
bootstrapApplication(AppComponent, {
  providers: [
    { provide: API_BASE_URL, useValue: 'https://api.example.com' },
  ],
});

// Inject
@Injectable({ providedIn: 'root' })
export class ApiService {
  private baseUrl = inject(API_BASE_URL);
}
```

---

## 5. Routing

### Lazy Loading Routes

```typescript
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),
  },
];
```

### Functional Guards and Resolvers

```typescript
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 },
  });
};

export const userResolver: ResolveFn<User> = (route) => {
  return inject(UserService).getUser(route.paramMap.get('id')!);
};

// In routes
{
  path: 'user/:id',
  loadComponent: () => import('./user.component'),
  canActivate: [authGuard],
  resolve: { user: userResolver },
}
```

---

## 6. Zoneless Angular

Zoneless apps skip zone.js entirely, yielding smaller bundles, cleaner stack traces, and better micro-frontend compatibility.

```typescript
// main.ts
import { provideZonelessChangeDetection } from '@angular/core';

bootstrapApplication(AppComponent, {
  providers: [provideZonelessChangeDetection()],
});
```

All components must use `ChangeDetectionStrategy.OnPush` and Signals for state. Signal updates automatically trigger change detection without zone.js.

---

## 7. SSR and Hydration

### Setup

```bash
ng add @angular/ssr
```

### Hydration Configuration

```typescript
// app.config.ts
import { provideClientHydration, withEventReplay } from '@angular/platform-browser';

export const appConfig: ApplicationConfig = {
  providers: [provideClientHydration(withEventReplay())],
};
```

### Incremental Hydration with @defer

```html
<app-hero />

@defer (hydrate on viewport) {
  <app-comments />
}

@defer (hydrate on interaction) {
  <app-chat-widget />
}
```

See §12 for the full `@defer` trigger reference.

### TransferState for SSR Data

Avoid refetching on the client data already fetched during server rendering. See `references/ssr.md` for the full `TransferState` service.

---

## 8. State Management

### Selection Criteria

```
Small app, simple state       -> Signal services
Medium app, moderate state    -> Component stores / NgRx SignalStore
Large app, complex flows      -> NgRx Store (actions, effects, devtools)
Heavy server interaction      -> NgRx Query + Signal services
```

See `references/state-management.md` for full examples: Signal Service, NgRx SignalStore, NgRx Store (actions/reducer/effects), and bridging Signals with RxJS.

---

## 9. Forms

### Reactive Forms (Preferred)

Use reactive forms with typed controls. See `references/forms.md` for the full form component with signal-tracked submit state and per-field validation.

---

## 10. HTTP Client and Interceptors

```typescript
// Functional interceptor
import { HttpInterceptorFn } from '@angular/common/http';

export const authInterceptor: HttpInterceptorFn = (req, next) => {
  const auth = inject(AuthService);
  const token = auth.getToken();
  if (token) {
    req = req.clone({ setHeaders: { Authorization: `Bearer ${token}` } });
  }
  return next(req);
};

// Provide in bootstrap
bootstrapApplication(AppComponent, {
  providers: [
    provideHttpClient(withInterceptors([authInterceptor])),
  ],
});
```

---

## 11. Performance Optimization

### Priority 1: Change Detection (CRITICAL)

- Always use `ChangeDetectionStrategy.OnPush`
- Use Signals for all component state
- Enable zoneless for new projects

### Priority 2: Eliminate Async Waterfalls (CRITICAL)

```typescript
// WRONG - Sequential fetching
this.userService.getUser(id).subscribe(user => {
  this.postsService.getPosts(user.id).subscribe(posts => { ... });
});

// CORRECT - Parallel
forkJoin({
  user: this.userService.getUser(id),
  posts: this.postsService.getPosts(id),
}).subscribe();

// CORRECT - Flatten dependent calls
this.route.params.pipe(
  map(p => p.id),
  switchMap(id => this.userService.getUser(id)),
).subscribe();
```

### Priority 3: Bundle Optimization (CRITICAL)

- Lazy load all feature routes with `loadComponent` / `loadChildren`
- Use `@defer` for heavy below-fold components (§12)
- Avoid barrel file re-exports (breaks tree-shaking)
- Dynamically import heavy third-party libraries

### Priority 4: Rendering Performance (HIGH)

```html
<!-- Always track by a stable identifier, not $index -->
@for (item of items(); track item.id) {
  <app-item [item]="item" />
}
```

- Use virtual scrolling (`CdkVirtualScrollViewport`) for large lists
- Use pure pipes instead of methods in templates
- Use `computed()` for derived data (memoized)
- Use `NgOptimizedImage` for images

### Priority 5: Memory Management

```typescript
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';

// Auto-cleanup subscriptions
this.data$.pipe(
  takeUntilDestroyed(inject(DestroyRef))
).subscribe();

// Better: convert to signal, no subscription needed
data = toSignal(this.service.data$, { initialValue: null });
```

---

## 12. Deferrable Views (@defer)

```html
@defer (on viewport) {
  <app-heavy-chart />
} @placeholder {
  <div class="skeleton"></div>
} @loading (minimum 200ms) {
  <app-spinner />
} @error {
  <p>Failed to load</p>
}
```

| Trigger          | When to Use                          |
|------------------|--------------------------------------|
| `on idle`        | Low-priority, hydrate when idle      |
| `on viewport`    | Hydrate when element enters viewport |
| `on interaction` | Hydrate on user interaction          |
| `on hover`       | Hydrate when user hovers             |
| `on timer(ms)`   | Hydrate after delay                  |

The same triggers drive incremental hydration in SSR (§7), written as `hydrate on <trigger>`.

Use `@defer` for: analytics charts, comment sections, chat widgets, below-fold content, heavy third-party components.

---

## 13. Component Composition

### Content Projection

```typescript
@Component({
  selector: 'app-card',
  template: `
    <div class="card">
      <div class="header">
        <ng-content select="[card-header]"></ng-content>
      </div>
      <div class="body">
        <ng-content></ng-content>
      </div>
    </div>
  `,
})
export class CardComponent {}
```

### Host Directives

```typescript
@Component({
  selector: 'app-button',
  standalone: true,
  hostDirectives: [
    { directive: TooltipDirective, inputs: ['tooltip: title'] },
  ],
  template: `<ng-content />`,
})
export class ButtonComponent {}
```

---

## 14. Testing

Test component behavior through the rendered template, not class internals. Test loading, error, and empty states explicitly for every async component.

- Unit: Jasmine + Karma or Jest.
- E2E (preferred for new projects): Playwright, or Cypress. Use `data-testid` attributes for stable selectors. Test critical user flows: navigation, form submission, error states.

See `testing-guidelines.md` for TestBed setup, signal-input testing (`componentRef.setInput`), HttpClient mocking, and full e2e examples.

---

## 15. UI State Patterns

### Loading State Rule

Show loading indicator ONLY when there is no data to display.

```html
@if (error()) {
  <app-error-state [error]="error()" (retry)="load()" />
} @else if (loading() && !items().length) {
  <app-skeleton-list />
} @else if (!items().length) {
  <app-empty-state message="No items found" />
} @else {
  <app-item-list [items]="items()" />
}
```

### Error Handling

```typescript
async save() {
  this.saving.set(true);
  try {
    await this.service.save(this.data);
    this.toast.success('Saved');
  } catch (error) {
    console.error('Save failed:', error);
    this.toast.error('Failed to save. Please try again.');
  } finally {
    this.saving.set(false);
  }
}
```

### Button States

Always disable buttons during async operations to prevent double submissions.

```html
<button (click)="save()" [disabled]="saving()">
  @if (saving()) { <app-spinner size="sm" /> Saving... }
  @else { Save }
</button>
```

---

## 16. Accessibility

- Use semantic HTML (`<nav>`, `<main>`, `<article>`, `<button>`)
- Add `aria-label` to icon-only buttons
- Link error messages to form fields with `aria-describedby`
- Manage focus after route changes and dialog open/close
- Announce loading states to screen readers (`aria-live="polite"`)
- Ensure color contrast meets WCAG 2.1 AA (4.5:1 text, 3:1 large)
- Support keyboard navigation for all interactive elements
- Test with axe-core / Lighthouse accessibility audits

---

## 17. Security

- Angular auto-escapes template interpolation (XSS protection)
- Never use `bypassSecurityTrustHtml` unless absolutely required
- Use `HttpClient` with XSRF/CSRF support: `provideHttpClient(withXsrfConfiguration())`
- Validate and sanitize user inputs server-side; do not rely on client-only validation
- Use Content Security Policy headers
- Avoid `innerHTML` binding; prefer template interpolation
- Use `HttpOnly`, `Secure`, `SameSite` cookie flags for auth tokens

---

## 18. Angular CLI and Project Structure

### Recommended Structure

```
src/
  app/
    core/              # Singleton services, guards, interceptors
    shared/            # Reusable components, pipes, directives
    features/
      dashboard/       # Feature: components, services, routes
      admin/
      user/
    app.component.ts
    app.config.ts
    app.routes.ts
  assets/
  environments/
```

### CLI Commands

```bash
ng generate component features/dashboard/dashboard --standalone
ng generate service core/auth
ng generate pipe shared/filter-active --standalone
ng generate directive shared/tooltip --standalone
ng add @angular/ssr
ng build --configuration=production
ng test
ng e2e
```

---

## 19. Styling

- Use Angular Material / CDK for component library needs
- Use `CdkVirtualScrollViewport` for virtual scrolling
- Use `CDK Dialog` or Angular Material `MatDialog` for modals
- Prefer component-scoped styles (default `ViewEncapsulation.Emulated`)
- Use CSS custom properties (variables) for theming
- Use `NgOptimizedImage` for responsive, optimized images

---

## Quick Reference: Do's and Don'ts

| Category           | Do                                     | Don't                                    |
|--------------------|----------------------------------------|------------------------------------------|
| State              | 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)            |
| Templates          | `@if`/`@for`/`@switch` control flow    | `*ngIf`/`*ngFor` structural directives   |
| Derived Data       | `computed()` (memoized)                | Getter methods (recalculates every CD)   |
| Subscriptions      | `toSignal()` or `takeUntilDestroyed`   | Manual subscribe/unsubscribe             |
| Template Methods   | Pure pipes or `computed()`             | Method calls in templates                |
| Error Handling     | Surface to user with toast/banner      | Silent `catch` with only `console.log`   |
| Buttons            | Disable during async operations        | Allow double submissions                 |
| Barrel Exports     | Direct file imports                    | Re-export everything from index.ts       |
| Forms              | Reactive forms with typed controls     | Untyped template-driven for complex UIs  |

---

## New Component Checklist

- [ ] `standalone: true`
- [ ] `changeDetection: ChangeDetectionStrategy.OnPush`
- [ ] Signals for state (`signal()`, `input()`, `output()`)
- [ ] `inject()` for dependencies
- [ ] `@for` with stable `track` expression
- [ ] Error, loading, and empty states handled
- [ ] Buttons disabled during async operations
- [ ] Accessibility: semantic HTML, ARIA, keyboard support

## Performance Review Checklist

- [ ] No methods called in templates (use pipes or `computed`)
- [ ] Large lists use virtual scrolling
- [ ] Heavy components use `@defer`
- [ ] All feature routes are lazy-loaded
- [ ] Heavy third-party libs dynamically imported
- [ ] SSR hydration configured with appropriate triggers

---

## 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 | Use signal updates, not direct property mutation      |
| SSR fetch fails                | Use `TransferState` or `withFetch()`                 |
| Memory leak                    | Use `takeUntilDestroyed` or convert to `toSignal`    |
| Slow list rendering            | Add `track` by stable ID, use virtual scrolling      |
| Bundle too large               | Lazy load routes, `@defer` heavy components, audit barrel files |

---

## TypeScript Standards for Angular

- Enable `strict: true` in tsconfig
- No `any` -- use proper interfaces and generics
- Use `import type` for type-only imports
- Define explicit return types on public service methods
- Colocate interfaces with their feature
- Use branded types or enums for domain identifiers
- Leverage TypeScript utility types (`Partial`, `Pick`, `Omit`, `Record`)

