Project Fit
| Attribute |
Value |
| Applies to |
frontend |
| Requires |
Angular, TailwindCSS, pnpm |
| Not for this repo |
React, Next.js, Vue, Svelte |
| Status |
✅ PRIMARY for Angular family |
Guardrails
Does NOT do:
- Install dependencies without user approval
- Modify pnpm-lock.yaml directly
- Run migrations automatically
Safety Checklist:
pnpm --filter @cermont/frontend lint
pnpm --filter @cermont/frontend test
pnpm --filter @cermont/frontend build
# Rollback: git restore -SW .
TypeScript
- Use strict type checking
- Prefer type inference when type is obvious
- Avoid
any; use unknown when type is uncertain
Components
- Always use standalone components (do NOT set
standalone: true — it's the default in v20+)
- Set
changeDetection: ChangeDetectionStrategy.OnPush
- Use
input() and output() functions instead of decorators
- Use
computed() for derived state
- Keep components small and single-responsibility
- Prefer inline templates for small components
- Use Reactive forms over Template-driven
- Use
class bindings instead of ngClass
- Use
style bindings instead of ngStyle
- For external templates/styles, use paths relative to the component TS file
- Do NOT use
@HostBinding/@HostListener — use the host object in the decorator instead
State Management with Signals
- Use signals for local component state
- Use
computed() for derived state
- Keep state transformations pure and predictable
- Do NOT use
mutate on signals — use update or set instead
For complex derived state patterns, see references/signal-patterns.md.
Resources (Async Data)
Use resource() for async data fetching with signals:
const userResource = resource({
params: () => ({ id: userId() }),
loader: ({ params, abortSignal }) => fetch(`/api/users/${params.id}`, { signal: abortSignal }),
});
const userName = computed(() => userResource.hasValue() ? userResource.value().name : undefined);
Key resource patterns:
params returns undefined → loader doesn't run, status becomes 'idle'
- Use
abortSignal to cancel in-flight requests
- Check
hasValue() before accessing value() to handle loading/error states
- Status values:
'idle', 'loading', 'reloading', 'resolved', 'error', 'local'
Templates
- Use native control flow:
@if, @for, @switch (NOT *ngIf, *ngFor, *ngSwitch)
- Use async pipe for observables
- Keep templates simple — no complex logic
- Do NOT use arrow functions in templates (not supported)
- Do NOT assume globals like
new Date() are available
Services
- Single responsibility per service
- Use
providedIn: 'root' for singletons
- Use
inject() function instead of constructor injection
@Injectable({ providedIn: 'root' })
export class UserService {
private readonly http = inject(HttpClient);
}
Routing
- Implement lazy loading for feature routes:
export const routes: Routes = [
{
path: 'admin',
loadComponent: () => import('./admin/admin.page').then(m => m.AdminPage),
},
];
File Naming
- Routable view components:
file-name.page.ts, file-name.page.html, file-name.page.css
- Regular components:
file-name.component.ts
- Services:
file-name.service.ts
Icons (ng-icon)
import { NgIcon, provideIcons } from '@ng-icons/core';
import { heroSparkles, heroTrash } from '@ng-icons/heroicons/outline';
@Component({
selector: 'app-example',
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [NgIcon],
providers: [provideIcons({ heroSparkles, heroTrash })],
template: `<ng-icon name="heroSparkles" />`,
})
export class ExampleComponent {}
Images
- Use
NgOptimizedImage for all static images
NgOptimizedImage does NOT work for inline base64 images
Styling
- Use Tailwind 4.1 for CSS (see tailwind skill if available)
- Angular CDK is available when needed
Accessibility
- MUST pass all AXE checks
- MUST meet WCAG AA minimums: focus management, color contrast, ARIA attributes
1---2name: cermont-frontend-angular-best-practices3description: Angular 21 development with modern best practices including signals, standalone components, reactive patterns, and accessibility. Use when creating Angular components, services, templates, or performing any Angular frontend development work. Covers TypeScript strict typing, signal-based state management, reactive forms, lazy loading, ng-icon setup, and Tailwind styling.4---5
6<!-- Cermont Project Fit -->
7## Project Fit
8
9| Attribute | Value |
10|-----------|-------|
11| **Applies to** | frontend |
12| **Requires** | Angular, TailwindCSS, pnpm |
13| **Not for this repo** | React, Next.js, Vue, Svelte |
14| **Status** | ✅ PRIMARY for Angular family |
15
16### Guardrails
17
18**Does NOT do:**
19- Install dependencies without user approval
20- Modify pnpm-lock.yaml directly
21- Run migrations automatically
22
23**Safety Checklist:**
24```bash
25pnpm --filter @cermont/frontend lint
26pnpm --filter @cermont/frontend test
27pnpm --filter @cermont/frontend build
28# Rollback: git restore -SW .
29```
30<!-- End Project Fit -->
31
32## TypeScript
33
34- Use strict type checking
35- Prefer type inference when type is obvious
36- Avoid `any`; use `unknown` when type is uncertain
37
38## Components
39
40- Always use standalone components (do NOT set `standalone: true` — it's the default in v20+)
41- Set `changeDetection: ChangeDetectionStrategy.OnPush`
42- Use `input()` and `output()` functions instead of decorators
43- Use `computed()` for derived state
44- Keep components small and single-responsibility
45- Prefer inline templates for small components
46- Use Reactive forms over Template-driven
47- Use `class` bindings instead of `ngClass`
48- Use `style` bindings instead of `ngStyle`
49- For external templates/styles, use paths relative to the component TS file
50- Do NOT use `@HostBinding`/`@HostListener` — use the `host` object in the decorator instead
51
52## State Management with Signals
53
54- Use signals for local component state
55- Use `computed()` for derived state
56- Keep state transformations pure and predictable
57- Do NOT use `mutate` on signals — use `update` or `set` instead
58
59For complex derived state patterns, see [references/signal-patterns.md](references/signal-patterns.md).
60
61## Resources (Async Data)
62
63Use `resource()` for async data fetching with signals:
64
65```typescript
66const userResource = resource({
67 params: () => ({ id: userId() }),
68 loader: ({ params, abortSignal }) => fetch(`/api/users/${params.id}`, { signal: abortSignal }),
69});
70
71const userName = computed(() => userResource.hasValue() ? userResource.value().name : undefined);
72```
73
74Key `resource` patterns:
75- `params` returns `undefined` → loader doesn't run, status becomes `'idle'`
76- Use `abortSignal` to cancel in-flight requests
77- Check `hasValue()` before accessing `value()` to handle loading/error states
78- Status values: `'idle'`, `'loading'`, `'reloading'`, `'resolved'`, `'error'`, `'local'`
79
80## Templates
81
82- Use native control flow: `@if`, `@for`, `@switch` (NOT `*ngIf`, `*ngFor`, `*ngSwitch`)
83- Use async pipe for observables
84- Keep templates simple — no complex logic
85- Do NOT use arrow functions in templates (not supported)
86- Do NOT assume globals like `new Date()` are available
87
88## Services
89
90- Single responsibility per service
91- Use `providedIn: 'root'` for singletons
92- Use `inject()` function instead of constructor injection
93
94```typescript
95@Injectable({ providedIn: 'root' })
96export class UserService {
97 private readonly http = inject(HttpClient);
98}
99```
100
101## Routing
102
103- Implement lazy loading for feature routes:
104
105```typescript
106export const routes: Routes = [
107 {
108 path: 'admin',
109 loadComponent: () => import('./admin/admin.page').then(m => m.AdminPage),
110 },
111];
112```
113
114## File Naming
115
116- Routable view components: `file-name.page.ts`, `file-name.page.html`, `file-name.page.css`
117- Regular components: `file-name.component.ts`
118- Services: `file-name.service.ts`
119
120## Icons (ng-icon)
121
122```typescript
123import { NgIcon, provideIcons } from '@ng-icons/core';
124import { heroSparkles, heroTrash } from '@ng-icons/heroicons/outline';
125
126@Component({
127 selector: 'app-example',
128 changeDetection: ChangeDetectionStrategy.OnPush,
129 imports: [NgIcon],
130 providers: [provideIcons({ heroSparkles, heroTrash })],
131 template: `<ng-icon name="heroSparkles" />`,
132})
133export class ExampleComponent {}
134```
135
136## Images
137
138- Use `NgOptimizedImage` for all static images
139- `NgOptimizedImage` does NOT work for inline base64 images
140
141## Styling
142
143- Use Tailwind 4.1 for CSS (see tailwind skill if available)
144- Angular CDK is available when needed
145
146## Accessibility
147
148- MUST pass all AXE checks
149- MUST meet WCAG AA minimums: focus management, color contrast, ARIA attributes