Angular Conventions — Framework Skill
Angular-specific rules for modern Angular: standalone, signals, block control flow, functional
providers. It gives the Angular form of rules that core-typescript and architecture-and-design
set in general terms.
Builds on.
core-typescript(language rules) andarchitecture-and-design(design), plusaccessibilityfor UI work. Load a sibling only when the task turns on its layer; if it is not loaded, apply that layer from general knowledge and do not block.
This SKILL.md is self-sufficient: the Ruleset below is the complete, enforceable list. Each
references/<topic>.md holds that group's reasoning and ❌ / ✅ code, and
references/worked-example.md a full review pass; open them for depth when your runtime allows.
How to Use This Skill
Pick the mode that matches the task. Do the steps in order.
| Mode | Steps |
|---|---|
| Generate — write a new component, service, or route | 1. Standalone, OnPush, inject(), signals (bootstrap, components, signals, inputs-outputs). 2. Block control flow in the template (templates). 3. Give every subscription a teardown (rxjs). 4. Run the Ruleset as a checklist. Fix each fail before you hand off. |
| Review — check a pull request or a diff | 1. Run the Ruleset against the diff. 2. Write one finding per fail, in the Output Format below. 3. Order the findings: must-fix first, then consider. 4. If nothing fails, say so in one line. Do not invent findings. |
| Migrate — modernize legacy Angular | 1. Run the official migration schematics first: standalone, control flow, inject, signal inputs, output, signal queries, lazy route loading. 2. Apply the Ruleset by hand for what a schematic left behind. 3. One migration per commit. Keep the tests green. |
Output Format
Write one finding per line:
<severity> · <topic> · <file>:<line> — <what is wrong>. <the fix as an action>.
<severity>ismust-fix(breaks a rule in this skill or a compiler check) orconsider(safe, but a rule prefers another form).<topic>is a Ruleset topic slug (signals,templates,rxjs, …).
Rules for Every Mode
- Name the Ruleset topic when you enforce a rule.
- Prefer the current Angular API over its decorator or NgModule predecessor.
- Consistency within a file wins. When a file already follows an older style throughout, match it and note the gap rather than half-converting it.
Ruleset
bootstrap → references/bootstrap.md
- No
NgModule— every component, directive, and pipe is standalone; no redundantstandalone: truewhere it is already the default. Each component lists what it uses in its ownimportsarray. - Bootstrap with
bootstrapApplication(App, appConfig)and functional providers (provideRouter,provideHttpClient,provideClientHydration); noprovideZoneChangeDetectionand nozone.jspolyfill in a new app —provideZonelessChangeDetection()only while upgrading a v20 app. -
angularCompilerOptionshasstrictTemplates,strictInjectionParameters,strictInputAccessModifiers,strictStandalone, andtypeCheckHostBindingson. -
angular-eslintruns (recommended+template/recommended+template/accessibility), withprefer-standalone,prefer-on-push-component-change-detection,use-lifecycle-interface,no-input-rename,template/prefer-control-flow, andtemplate/prefer-ngsrcon.
components → references/components.md
-
ChangeDetectionStrategy.OnPushon every component. - Dependencies come from
inject(), not constructor parameters. - Injected members, inputs, outputs, and queries are grouped at the top of the class.
- Every Angular-assigned member (
input(),model(),output(), a query) isreadonly; a template-only member isprotected. - One component / directive / service per file, named for the class (the current style guide drops the
.componentsuffix — match the codebase if it still uses it). - Selectors carry the single project prefix; a directive uses an attribute selector.
- Each lifecycle hook is kept short and implements its interface (
OnInit,OnDestroy). - Host bindings and listeners go in the
hostobject, not@HostBinding/@HostListener. - An event handler is named for the action (
saveDraft()), not the event (onClick()). - Cross-cutting behavior is a
hostDirective, not a base class or copy-paste. - No
::ng-deep;ViewEncapsulation.Noneonly in a clearly-named global file; a child-piercing override is scoped with:has().
signals → references/signals.md
- State is in
signal(); every dependent value is acomputed(), kept pure. - No signal write inside
effect()— usecomputed()orlinkedSignal(). Aneffect()only pushes a signal value into a non-reactive API (logging,localStorage, a canvas, a widget) and releases its resource inonCleanup. -
untracked()wraps a read that must not become a dependency. - No
cdr.detectChanges()ormarkForCheck()after a signal write;cdr.detectChanges()only for an imperative non-signal change Angular cannot observe (and that field should become a signal). - Shared state is a
providedIn: 'root'service exposingsignal/computedmembers; a store library only once that service grows entities, effects, and derived collections (architecture-and-design, state-and-data).
inputs-outputs → references/inputs-outputs.md
-
input()/input.required()/output()/model()/viewChild()/contentChild()— no@Input()/@Output()/@ViewChild/@ContentChilddecorators. - An input is read as a call (
this.name());input.required<T>()over an optional input plus a?guard; a two-way value is written withthis.value.set(...).
templates → references/templates.md
-
@if/@for/@switch, not*ngIf/*ngFor/*ngSwitch; every@forhas atrackon a stable id, not$index. - No method call in a binding — a
computed()or a pure pipe; any expression past a property read or one pipe is moved into acomputed();@letfor a value read more than once. -
@deferwith anontrigger around a heavy or below-the-fold section. -
[class.x]/[style.x.px]overNgClass/NgStyle. - Caller-supplied content comes through
<ng-content>+ named slots, aTemplateRefinput +*ngTemplateOutlet, orNgComponentOutlet; the slot-vs-prop decision iscomponent-api-design, slots-vs-config. - A custom pipe is pure, standalone, typed, and does no I/O.
dependency-injection → references/dependency-injection.md
- A service is
@Injectable({ providedIn: 'root' }); a narrower scope only when the instance must be per-route or per-component. - Route guards are functional (
CanActivateFn); HTTP interceptors are functional (HttpInterceptorFnwithwithInterceptors). - A system boundary injects an
InjectionToken<T>for an abstraction, not a concrete class (architecture-and-design, solid — DIP, and patterns). -
inject()is called only in an injection context (constructor, field initializer,provide*factory); a later call is wrapped inrunInInjectionContext. - Manual teardown is tied to
inject(DestroyRef), not anngOnDestroybookkeeping field.
rxjs → references/rxjs.md
- A signal for state; an Observable for a stream over time (HTTP, router events, WebSocket, DOM events), converted to a signal at the edge with
toSignal(). - No manual
.subscribe()withouttakeUntilDestroyed()(in an injection context or passed aDestroyRef) or theasyncpipe. -
HttpClientis called from a repository, not a component (architecture-and-design, patterns); server state is cached with a cache library, not a hand-rolledBehaviorSubjectstore (architecture-and-design, state-and-data). - A component-level server read is a
resource()/httpResource()(stable since v22);toSignal()for a non-resource stream; a cache library once several components need dedup and invalidation of the same data. - Transport errors are handled in one functional interceptor (status → domain error, backoff retry for an idempotent call); one top-level
ErrorHandlerreports and shows a fallback (architecture-and-design, frontend-practices).
forms → references/forms.md
- A new form in a signal-based component uses Signal Forms (stable since v22); reactive forms where the codebase is already built on them; template-driven only for a trivial single input.
- A reactive form is typed (
new FormControl<string>('', { nonNullable: true })); no untypedFormGrouporFormControl. - Validators are built from the same schema as the domain model (
architecture-and-design, forms). -
invalid/dirty/ error text are derived from form state, not copied into signals. - A control a mode does not render is
disable({ emitEvent: false })d, not@if-hidden. - A blocked save shows the user why — a toast or an inline message, not only a disabled button.
routing → references/routing.md
-
provideRouter(routes); a feature is lazy-loaded withloadComponent/loadChildrenand maps to a feature folder (architecture-and-design, structure). - A feature-only service is scoped in the route's
providersarray, notprovidedIn: 'root'. - Route params, query params, and data are bound to inputs with
withComponentInputBinding(). - Filters, the current tab, and pagination are kept in the URL (
architecture-and-design, state-and-data).
rendering-ssr → references/rendering-ssr.md
- Zoneless-ready: the view is driven by signals or the
asyncpipe, never a change-detection side effect. - DOM measurement and imperative DOM work go in
afterNextRender()/afterEveryRender(), notngAfterViewInit; DOM changes go throughRenderer2or a binding, notElementRef.nativeElement+document. - No
window/document/localStoragein a constructor or field initializer — guarded withafterNextRenderorisPlatformBrowser. -
provideClientHydration()for an SSR app;NgOptimizedImage(ngSrc) with explicitwidth/heightorfill. - No
bypassSecurityTrust*on anything a user or an API supplied (architecture-and-design, security). - Focus and live-change announcement use the CDK a11y tools (
FocusTrap,FocusMonitor,LiveAnnouncer); full lens:accessibility. - In an app still on Zone.js, a high-frequency listener (
scroll,mousemove,rAF) runs insideNgZone.runOutsideAngular.
testing → references/testing.md
- A standalone component is tested through its own imports —
TestBed.configureTestingModule({ imports: [C] })or@testing-library/angular'srender(C, …). -
provideHttpClientTesting()and assertions onHttpTestingController; the network is never hit. - Navigation is driven by
RouterTestingHarness, not a hand-builtActivatedRoutestub. - The DOM is queried by role and accessible name (a CDK harness or
@testing-library/angular'sscreen.getByRole), never a raw CSS selector onDebugElement/nativeElement. - A
signal/computedis read afterfixture.detectChanges(); real providers, mocks only at the network boundary and at a true external service. -
fakeAsync/tickonly whenawait fixture.whenStable()cannot do it; a resolved promise chain isawaited before a synchronous assertion. - Overlay content (dialog, dropdown) is asserted via its controlling signal or a
documentquery, notfixture.nativeElement. - Each test also passes the
test-qualityRuleset — asserts on behavior not internals, has a meaningful assertion, is deterministic. This group is the Angular mechanics;test-qualityjudges the test itself.
Limits
This skill is Angular framework rules. It does not cover:
- Language rules (see
core-typescript) or framework-neutral architecture (seearchitecture-and-design). - Deep RxJS operator design, and store libraries (NgRx, NGXS) — use the state tiers in
architecture-and-design, state-and-data, and reach for a store only when they call for one. - Nx or monorepo setup, Angular Material theming, and
@angular/animations. Styling isstyling-and-design-tokens; i18n (@angular/localizepolicy) isi18n-and-localization; loading and interaction cost isweb-performance. - Accessibility depth — CDK a11y usage is noted where it fits, but focus management, ARIA, and a11y testing live in
accessibility. - Angular versions before standalone components and block control flow. For a legacy app, migrate first (the Migrate mode above).
- Other frameworks —
reactandvueare the sibling skills; every rule here is Angular-specific.
Zoneless change detection is stable and the default from v21. The rules here keep an app still on Zone.js zoneless-ready.
References
This skill composes with:
core-typescript— the language base; Angular templates and DI do not exempt code from it.architecture-and-design— the design layer. On a conflict it decides the design, this skill decides the Angular API.accessibility— the review lens for UI; Angular's tools are the CDKa11ypackage andLiveAnnouncer.test-quality— judges the individual test this skill'stestinggroup produces.react/vue— the sibling framework skills.