angular-agent-skill

Installation
SKILL.md

Angular Development

Expert guidance for building scalable Angular applications using modern APIs: signals, standalone components, built-in control flow, and strict TypeScript.

Core Architecture

Project Structure

src/
└── app/
    ├── core/           # Non-business features (layout, auth, guards)
    ├── features/       # Business domain features
    ├── shared/         # Reusable "dumb" components, pipes, directives
    ├── services/       # Global and feature-specific services
    ├── interfaces/     # TypeScript interfaces
    ├── types/          # Type definitions, DTOs (requests/, responses/)
    ├── enums/          # Enums
    ├── utils/          # Pure utility functions
    ├── app.component.ts
    ├── app.config.ts
    └── app.routes.ts

Feature folders follow: pages/ (routed), components/ (non-routed), guards/, feature.routes.ts.

Coding Standards

  • Single quotes, 2-space indent, const by default
  • Kebab-case file names with Angular suffixes (.component.ts, .service.ts)
  • Strict typing, never use any — define explicit types
  • Optional chaining (?.) and nullish coalescing (??)
  • Template literals for string interpolation
  • Always declare methods and properties explicitly as public or private — never rely on implicit public
  • Prefix private properties with underscore: private _state, private readonly _http = inject(HttpClient)
  • Use private readonly for all DI injections: private readonly _http = inject(HttpClient)
  • File structure: imports, class, properties, lifecycle, public methods, private methods

Signals (State Management)

Use Angular's signal system as the primary state management approach.

Signal Primitives

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

// Read-only exposure
private readonly _count = signal(0);
readonly count = this._count.asReadonly();

// Computed (lazy, memoized, read-only)
doubleCount = computed(() => this.count() * 2);

// Custom equality for objects
data = signal(['test'], { equal: (a, b) => a.length === b.length });

Signal Inputs, Outputs & Queries

Use function-based APIs instead of decorators:

// Inputs
value = input(0);                                    // InputSignal<number>
name = input<string>();                              // InputSignal<string | undefined>
label = input.required<string>();                    // Required, no undefined
disabled = input(false, { transform: booleanAttribute });

// Outputs
closed = output<void>();                             // OutputEmitterRef<void>
valueChanged = output<number>();
this.valueChanged.emit(42);

// Model inputs (two-way binding)
checked = model(false);                              // ModelSignal<boolean>
// Parent: <my-comp [(checked)]="isChecked" />

// Queries
header = viewChild(HeaderComponent);                 // Signal<HeaderComponent | undefined>
items = viewChildren(ItemComponent);                 // Signal<readonly ItemComponent[]>
toggle = contentChild.required(ToggleComponent);     // Signal<ToggleComponent>

linkedSignal

Writable signal that resets when dependencies change. Use for selections that depend on changing data:

// Shorthand
selectedOption = linkedSignal(() => this.options()[0]);

// Full form — preserves selection across data changes
selectedOption = linkedSignal({
  source: this.options,
  computation: (newOptions, previous) =>
    newOptions.find(o => o.id === previous?.value.id) ?? newOptions[0],
});

resource / httpResource

Integrate async data into signals. Experimental but ready for use:

userResource = resource({
  params: () => ({ id: this.userId() }),
  loader: ({ params, abortSignal }) =>
    fetch(`/api/users/${params.id}`, { signal: abortSignal }).then(r => r.json()),
});

// Access: userResource.value(), .status(), .isLoading(), .error(), .hasValue()
// Actions: userResource.reload(), .set(), .update()

effect

For side effects only — logging, localStorage sync, DOM interop. Never use for state propagation (use computed or linkedSignal instead):

constructor() {
  effect((onCleanup) => {
    const user = this.currentUser();
    const timer = setTimeout(() => console.log(user), 1000);
    onCleanup(() => clearTimeout(timer));
  });
}

RxJS Interop

// Observable → Signal
counter = toSignal(this.counter$, { initialValue: 0 });
// Use requireSync: true for BehaviorSubject

// Signal → Observable
query$ = toObservable(this.query);

For detailed signal patterns, see references/signals.md.

Templates

Built-in Control Flow

Always use the built-in block syntax. Never use *ngIf, *ngFor, *ngSwitch:

@if (user(); as user) {
  <h1>{{ user.name }}</h1>
} @else {
  <p>Loading...</p>
}

@for (item of items(); track item.id) {
  <li>{{ item.name }}</li>
} @empty {
  <li>No items found.</li>
}

@switch (role()) {
  @case ('admin') { <admin-panel /> }
  @case ('editor') { <editor-panel /> }
  @default { <viewer-panel /> }
}

@for requires track. Use a unique identifier; use $index only for static lists. Available context: $index, $first, $last, $even, $odd, $count.

Deferrable Views

Reduce initial bundle size by deferring non-critical content:

@defer (on viewport; prefetch on idle) {
  <heavy-chart [data]="chartData()" />
} @placeholder (minimum 200ms) {
  <div class="skeleton"></div>
} @loading (after 100ms; minimum 500ms) {
  <spinner />
} @error {
  <p>Failed to load chart.</p>
}

Triggers: on idle, on viewport, on interaction, on hover, on timer(500ms), on immediate, when condition.

For detailed template patterns, see references/templates.md.

Components & DI

Standalone by Default

All components are standalone by default (Angular 19+). Import dependencies directly:

@Component({
  selector: 'app-user-card',
  imports: [DatePipe, RouterLink],
  templateUrl: './user-card.component.html',
  styleUrl: './user-card.component.css',
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class UserCardComponent {
  user = input.required<User>();
  selected = output<void>();
}

Dependency Injection

Use inject() function, not constructor injection. Always use private readonly with underscore prefix:

export class UserService {
  private readonly _http = inject(HttpClient);
  private readonly _config = inject(ConfigService);
}

Services use providedIn: 'root' for tree-shakeable singletons.

HTTP

Convert observables to promises with firstValueFrom:

public async getUser(id: string): Promise<User> {
  return firstValueFrom(
    this._http.get<User>(`/api/users/${id}`)
  );
}

Configure with functional API:

provideHttpClient(
  withInterceptors([authInterceptor]),
  withFetch(),  // Use Fetch API, required for SSR streaming
)

Functional Interceptors

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

Lifecycle & Cleanup

  • Use afterNextRender() / afterRenderEffect() for DOM operations (browser-only)
  • Prefer DestroyRef + takeUntilDestroyed() for subscription cleanup:
// In constructor (injection context) — no argument needed
this.route.params.pipe(takeUntilDestroyed()).subscribe(/*...*/);

// Outside constructor — pass DestroyRef
private readonly _destroyRef = inject(DestroyRef);
public ngOnInit() {
  this.obs$.pipe(takeUntilDestroyed(this._destroyRef)).subscribe(/*...*/);
}
  • Render phases: earlyReadwritemixedReadWriteread

Routing

Use functional guards and resolvers. Lazy-load feature routes:

export const routes: Routes = [
  {
    path: 'dashboard',
    loadComponent: () => import('./features/dashboard/pages/dashboard.component'),
    canActivate: [() => inject(AuthService).isAuthenticated()],
  },
  {
    path: 'user/:id',
    loadComponent: () => import('./features/user/pages/user-detail.component'),
    resolve: { user: (route: ActivatedRouteSnapshot) => inject(UserService).getUser(route.paramMap.get('id')!) },
  },
];

Enable withComponentInputBinding() to automatically bind route params, query params, and resolve data to component inputs:

// app.config.ts
provideRouter(routes, withComponentInputBinding(), withViewTransitions())

// Component — 'id' auto-bound from route param ':id', 'user' from resolve
export class UserDetailComponent {
  id = input<string>();
  user = input<User>();
}

App Configuration

// app.config.ts
export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes, withComponentInputBinding(), withViewTransitions()),
    provideHttpClient(withInterceptors([authInterceptor]), withFetch()),
    provideAnimationsAsync(),
    provideClientHydration(withEventReplay()),
    // provideExperimentalZonelessChangeDetection(), // Zoneless (experimental)
  ],
};

Performance

  • ChangeDetectionStrategy.OnPush on all components
  • track with unique IDs in @for loops
  • @defer for below-fold and heavy components
  • NgOptimizedImage for all images with priority on LCP image
  • Pure pipes for repeated template computations
  • Signals automatically optimize re-renders with granular tracking
  • takeUntilDestroyed() to prevent subscription memory leaks

Accessibility (a11y)

Use semantic HTML first. Add ARIA attributes only when native elements aren't sufficient. Always bind ARIA attributes with [attr.aria-*] (not [aria-*]) since they have no DOM properties.

@angular/aria — Headless Accessible Components

For common interactive UI patterns, use @angular/aria (npm install @angular/aria). These headless directives implement WAI-ARIA patterns with full keyboard navigation, ARIA attributes, and focus management built in — you provide HTML structure and CSS styling.

Available components:

Need Component Import
Collapsible sections Accordion @angular/aria/accordion
Tabbed panels Tabs @angular/aria/tabs
Selection list Listbox @angular/aria/listbox
Dropdown (<20 options) Select @angular/aria/combobox + listbox
Multi-select dropdown Multiselect @angular/aria/combobox + listbox
Searchable dropdown Autocomplete @angular/aria/combobox + listbox
Context/action menu Menu @angular/aria/menu
Navigation bar Menubar @angular/aria/menu
Grouped actions Toolbar @angular/aria/toolbar
Interactive table/calendar Grid @angular/aria/grid
Hierarchical data Tree @angular/aria/tree

All components are headless (bring your own styles), support RTL, use Angular signals for state, and import directly into standalone components.

// Example: Accessible tabs with @angular/aria
import { Tabs, TabList, Tab, TabPanel, TabContent } from '@angular/aria/tabs';

@Component({
  imports: [Tabs, TabList, Tab, TabPanel, TabContent],
  template: `
    <div ngTabs>
      <div ngTabList selectionMode="follow" selectedTab="general">
        <div ngTab value="general">General</div>
        <div ngTab value="security">Security</div>
      </div>
      <div ngTabPanel [preserveContent]="true" value="general">
        <ng-template ngTabContent>General settings</ng-template>
      </div>
      <div ngTabPanel [preserveContent]="true" value="security">
        <ng-template ngTabContent>Security settings</ng-template>
      </div>
    </div>
  `,
})

For detailed API reference, inputs, keyboard interactions, and examples for all components, see references/angular-aria.md.

CDK a11y Module

For lower-level accessibility utilities (focus traps, focus monitoring, screen reader announcements, custom keyboard navigation), use @angular/cdk/a11y. Services are providedIn: 'root' — no module import needed:

import { CdkTrapFocus, CdkMonitorFocus, CdkAriaLive } from '@angular/cdk/a11y';

Focus Management

Use CdkTrapFocus for modals/dialogs to trap Tab focus. Use cdkFocusInitial to mark the initial focus target:

<div role="dialog" aria-modal="true" [attr.aria-labelledby]="titleId"
     cdkTrapFocus [cdkTrapFocusAutoCapture]="true">
  <h2 [id]="titleId">{{ title() }}</h2>
  <input cdkFocusInitial />
  <button (click)="close()">Close</button>
</div>

Use FocusMonitor to detect how an element received focus and show keyboard-only focus rings:

private readonly _focusMonitor = inject(FocusMonitor);
// _focusMonitor.monitor(el) → Observable<FocusOrigin>
// _focusMonitor.focusVia(el, 'keyboard') → programmatic focus with origin

Screen Reader Announcements

Use LiveAnnouncer for dynamic status updates:

private readonly _liveAnnouncer = inject(LiveAnnouncer);
this._liveAnnouncer.announce('Item saved', 'polite');    // Waits for current speech
this._liveAnnouncer.announce('Error occurred', 'assertive'); // Interrupts

Or CdkAriaLive in templates: <div [cdkAriaLive]="'polite'">{{ status() }}</div>

Keyboard Navigation

FocusKeyManager — roving tabindex pattern (moves DOM focus between items):

keyManager = new FocusKeyManager(this.items()).withWrap().withVerticalOrientation();
// Items implement: focus(), disabled?, getLabel?()

ActiveDescendantKeyManager — uses aria-activedescendant (focus stays on host, e.g. combobox):

keyManager = new ActiveDescendantKeyManager(this.options()).withWrap().withTypeAhead();
// Items implement: setActiveStyles(), setInactiveStyles(), disabled?
// Host: [attr.aria-activedescendant]="keyManager.activeItem?.id"

Accessible Forms

<label for="email">Email</label>
<input id="email" type="email"
  [attr.aria-invalid]="emailInvalid() ? 'true' : null"
  [attr.aria-describedby]="emailInvalid() ? 'email-error' : null"
  [attr.aria-required]="'true'" />
@if (emailInvalid()) {
  <span id="email-error" role="alert">Please enter a valid email.</span>
}

For detailed CDK a11y patterns and full API reference, see references/accessibility.md.

Security

  • Never use innerHTML — rely on Angular's built-in sanitization
  • Sanitize dynamic content with Angular's DomSanitizer when necessary

Testing

  • Arrange-Act-Assert pattern
  • Test services, components, and utilities with high coverage

SSR / Hybrid Rendering

Configure per-route rendering in app.routes.server.ts:

export const serverRoutes: ServerRoute[] = [
  { path: '', renderMode: RenderMode.Client },
  { path: 'about', renderMode: RenderMode.Prerender },
  { path: 'profile', renderMode: RenderMode.Server },
];

Use afterNextRender() for browser-only code. @defer blocks render @placeholder on server.

References

  • references/signals.md — Detailed signal API reference, patterns for resource, linkedSignal, effect cleanup, and RxJS interop
  • references/templates.md — Control flow details, @defer triggers, SSR considerations, and template best practices
  • references/angular-aria.md@angular/aria headless accessible components: Accordion, Tabs, Listbox, Select, Multiselect, Autocomplete, Combobox, Menu, Menubar, Toolbar, Grid, Tree — with full API, inputs, keyboard interactions, and examples
  • references/accessibility.md — CDK a11y utilities: FocusTrap, FocusMonitor, LiveAnnouncer, AriaDescriber, KeyManagers, InteractivityChecker, InputModalityDetector, HighContrastModeDetector, and accessible component patterns
Weekly Installs
10
First Seen
Feb 19, 2026
Installed on
opencode10
gemini-cli10
github-copilot10
codex10
amp10
kimi-cli10