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, andbrain/knowledge/testing.md. Always apply those principles alongside the framework-specific guidance below. Language-Specific Testing: Seetesting-guidelines.mdin 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.
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
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
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.
@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
// 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.
@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
@Component({...})
export class UserComponent {
private http = inject(HttpClient);
private userService = inject(UserService);
users = toSignal(this.userService.getUsers());
}
Injection Tokens
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
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
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.
// 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
ng add @angular/ssr
Hydration Configuration
// app.config.ts
import { provideClientHydration, withEventReplay } from '@angular/platform-browser';
export const appConfig: ApplicationConfig = {
providers: [provideClientHydration(withEventReplay())],
};
Incremental Hydration with @defer
<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
// 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)
// 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
@deferfor heavy below-fold components (§12) - Avoid barrel file re-exports (breaks tree-shaking)
- Dynamically import heavy third-party libraries
Priority 4: Rendering Performance (HIGH)
<!-- 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
NgOptimizedImagefor images
Priority 5: Memory Management
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)
@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
@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
@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-testidattributes 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.
@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
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.
<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-labelto 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
bypassSecurityTrustHtmlunless absolutely required - Use
HttpClientwith 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
innerHTMLbinding; prefer template interpolation - Use
HttpOnly,Secure,SameSitecookie 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
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
CdkVirtualScrollViewportfor virtual scrolling - Use
CDK Dialogor Angular MaterialMatDialogfor modals - Prefer component-scoped styles (default
ViewEncapsulation.Emulated) - Use CSS custom properties (variables) for theming
- Use
NgOptimizedImagefor 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 -
@forwith stabletrackexpression - 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: truein tsconfig - No
any-- use proper interfaces and generics - Use
import typefor 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)