|
| 1 | +--- |
| 2 | +description: Angular 20 best practices and coding standards for the projects/web application. |
| 3 | +globs: ['projects/web/**/*.{ts,html,scss,css}'] |
| 4 | +--- |
| 5 | + |
| 6 | +# Angular 20 Best Practices — `projects/web` |
| 7 | + |
| 8 | +## Project Structure |
| 9 | + |
| 10 | +- Source root: `projects/web/src/` |
| 11 | +- App code: `src/app/` (feature modules), `src/common/` (shared), `src/orchestration/` |
| 12 | +- Path aliases: `@app/*`, `@common/*`, `@api-clients/*`, `@assets/*`, `@tests/*`, `orchestration/*` |
| 13 | +- Always use path aliases for cross-directory imports; use relative imports only within the same feature folder. |
| 14 | + |
| 15 | +## TypeScript |
| 16 | + |
| 17 | +- **Strict mode is NOT enabled** — `tsconfig.json` has `strict: false`, `noImplicitAny: false`, `strictNullChecks: false`. Do not assume strict checks. Be defensive with null/undefined handling. Existing `any` usage exists but should not be introduced in new code. |
| 18 | +- For TypeScript guidelines (inference, avoid any, interfaces, no magic numbers, no console.log) see **code-quality** skill. |
| 19 | + |
| 20 | +## UI Library: `@swimlane/ngx-ui` |
| 21 | + |
| 22 | +**Always use Swimlane ngx-ui controls** — never recreate standard UI with raw `<div>`/`<span>` + ARIA when an ngx-ui component exists. |
| 23 | +Docs: [https://swimlane.github.io/ngx-ui/](https://swimlane.github.io/ngx-ui/) |
| 24 | + |
| 25 | +| Category | Components | |
| 26 | +| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | |
| 27 | +| **Buttons** | `ngx-button`, `ngx-button-toggle`, `ngx-button-toggle-group`, `ngx-long-press-button`, `ngx-plus-menu` | |
| 28 | +| **Form controls** | `ngx-input`, `ngx-input-prefix`, `ngx-input-suffix`, `ngx-input-hint`, `ngx-select`, `ngx-select-option`, `ngx-checkbox`, `ngx-toggle`, `ngx-radiobutton`, `ngx-radiobutton-group`, `ngx-slider`, `ngx-datetime`, `ngx-date-range-picker`, `ngx-codemirror` | |
| 29 | +| **Layout** | `ngx-card` (+ `ngx-card-header`, `ngx-card-body`, `ngx-card-footer`, `ngx-card-title`, `ngx-card-subtitle`, `ngx-card-avatar`, `ngx-card-tag`, `ngx-card-section`), `ngx-section`, `ngx-section-header`, `ngx-tabs`, `ngx-tab`, `ngx-toolbar`, `ngx-toolbar-content`, `ngx-split`, `ngx-split-area` | |
| 30 | +| **Navigation** | `ngx-dropdown` (+ `ngx-dropdown-toggle`, `ngx-dropdown-menu`), `ngx-navbar`, `ngx-navbar-item`, `ngx-nav-menu`, `ngx-stepper`, `ngx-step` | |
| 31 | +| **Overlays** | `ngx-dialog`, `ngx-large-format-dialog-content`, `ngx-large-format-dialog-footer`, `ngx-drawer`, `ngx-dialog-drawer-content`, `ngx-overlay` | |
| 32 | +| **Feedback** | `ngx-notification`, `ngx-nag`, `ngx-loading`, `ngx-progress-spinner`, `ngx-tip`, `ngx-alert` | |
| 33 | +| **Data** | `ngx-datatable`, `ngx-datatable-column`, `ngx-tree`, `ngx-tree-node`, `ngx-list`, `ngx-json-editor`, `ngx-json-editor-flat` | |
| 34 | +| **Directives** | `ngx-tooltip` (attribute), `[autosize]`, `[dblClickCopy]`, `[long-press]`, `[resizeObserver]` | |
| 35 | +| **Icons** | `ngx-icon` (726 usages — the most used component) | |
| 36 | + |
| 37 | +- If an ngx-ui component exists for the need, **use it**. Do not build custom buttons, inputs, dropdowns, dialogs, or tabs from scratch. |
| 38 | +- Import components from `@swimlane/ngx-ui` in the component's `imports` array. |
| 39 | + |
| 40 | +## Components |
| 41 | + |
| 42 | +- **Any new component is standalone** — do not create non-standalone components or add new components to `NgModules`. Declare dependencies in the component’s `imports` array and import the component where it is used (or via a barrel that re-exports it). |
| 43 | +- **Implicit standalone** — do NOT set `standalone: true` in the decorator; it is the default in Angular 20. |
| 44 | +- **`ChangeDetectionStrategy.OnPush`** — required on all components. |
| 45 | +- **External templates** — use `templateUrl` with a separate `.html` file. Inline templates are not the convention here. |
| 46 | +- **Host bindings** — use the `host` object in the decorator, NOT `@HostBinding` / `@HostListener`. |
| 47 | +- **No `ngClass` / `ngStyle`** — use native `[class.active]="flag"` and `[style.font-size.px]="size"` bindings. |
| 48 | +- For structure and signal inputs/outputs see **angular-component** skill. |
| 49 | + |
| 50 | +## Code Style |
| 51 | + |
| 52 | +See **code-quality** skill (max ~30 lines per method, max 3 parameters, `private`, single responsibility, no business logic in components). |
| 53 | + |
| 54 | +## Dependency Injection |
| 55 | + |
| 56 | +- **New code:** prefer the `inject()` function. Mark injected services as `private readonly`. |
| 57 | +- **Existing code:** constructor injection is prevalent (~460 components). Do not rewrite working constructor injection unless refactoring the component. |
| 58 | +- **Do not mix** `inject()` and constructor injection within the same class. |
| 59 | +- See **angular-di** skill for tokens and providers. |
| 60 | + |
| 61 | +## Signals & Reactivity |
| 62 | + |
| 63 | +- **`input()` / `output()`** — use signal-based inputs and outputs for new components. |
| 64 | +- **`signal()` / `computed()`** — use for local component state and derived values. |
| 65 | +- **`effect()`** — use for signal-based side effects (e.g., logging, syncing to localStorage). Avoid heavy logic inside effects; keep them lean. |
| 66 | +- **`viewChild()` / `viewChildren()` / `contentChild()` / `contentChildren()`** — use signal-based queries instead of the `@ViewChild` / `@ContentChild` decorators. |
| 67 | +- **`linkedSignal()`** — available in Angular 20 for two-way derived signals (e.g. a writable signal that resets when a parent signal changes). |
| 68 | +- **`resource()`** — available in Angular 20 for declarative async data loading tied to signals. |
| 69 | +- **Signal updates** — use `set()` or `update()`, never `mutate()`. |
| 70 | +- Adoption is growing (~30 components). Prefer signals for all new component state. |
| 71 | +- See **angular-signals** skill for patterns. |
| 72 | + |
| 73 | +## Templates |
| 74 | + |
| 75 | +- **No inline logic in templates** — do not put expressions or method calls directly in template bindings. Always define a method in the component `.ts` file with a meaningful name and call it from the template. This keeps templates readable and logic testable. |
| 76 | +- **Native control flow** — always use `@if`, `@for`, `@switch`, `@empty`. **Never** use `*ngIf`, `*ngFor`, `*ngSwitch`, or any structural directive syntax in new code. The codebase has fully migrated (~4,400 usages, <25 legacy instances remaining — do not add more). |
| 77 | +- Do NOT import `CommonModule`, `NgIf`, `NgFor`, or `NgSwitch` in new components — they are not needed with built-in control flow. |
| 78 | +- **`@for` track** — always provide a `track` expression. Prefer `track item.id` over `track $index`. |
| 79 | +- **`@defer`** — use for lazy-loading heavy template sections. |
| 80 | +- **Async pipe or `toSignal()`** — never manually subscribe in templates. Use `async` pipe for observables or convert with `toSignal()`. |
| 81 | +- **Accessibility** — see the dedicated `accessibility.mdc` rule for full WCAG 2.2 AA standards. |
| 82 | +- See **angular-component** and **angular-signals** skills for template patterns. |
| 83 | + |
| 84 | +## Subscriptions |
| 85 | + |
| 86 | +- Never subscribe manually in components — use `async` pipe or `toSignal()`. |
| 87 | +- If you must subscribe in a service, always clean up with `takeUntilDestroyed()` or `DestroyRef`. |
| 88 | + |
| 89 | +## Async: Prefer RxJS over Promises |
| 90 | + |
| 91 | +Prefer RxJS Observables over Promises for all async APIs, data flows, and service methods. The codebase has undergone significant refactoring to eliminate Promise-based APIs; do not introduce new ones. See **angular-http** skill (references: Prefer Observables over Promises). |
| 92 | + |
| 93 | +## Services & HTTP |
| 94 | + |
| 95 | +- **`providedIn: 'root'`** for singleton services. |
| 96 | +- **Single responsibility** — one service, one concern. |
| 97 | +- **`inject()` function** preferred for new services. |
| 98 | +- **`HttpClient`** — use with typed responses. Handle errors with RxJS `catchError` or `tapResponse` in stores. |
| 99 | +- **Interceptors** — use `HttpInterceptorFn` (functional) for cross-cutting concerns (auth headers, error handling, CSRF). |
| 100 | +- **Caching** — use `shareReplay({ bufferSize: 1, refCount: true })` for shared observables that shouldn't re-fetch. |
| 101 | +- See **angular-http** and **angular-di** skills. |
| 102 | + |
| 103 | +## Reactive Forms |
| 104 | + |
| 105 | +- Prefer Reactive Forms (`FormGroup`, `FormControl`) over template-driven forms. |
| 106 | +- Use typed forms (`FormGroup<{ name: FormControl<string> }>`). |
| 107 | +- Use built-in and custom `ValidatorFn` / `AsyncValidatorFn` for validation — keep validation logic in the form definition, not the template. |
| 108 | +- See **angular-forms** skill (including Signal Forms reference). |
| 109 | + |
| 110 | +## Testing |
| 111 | + |
| 112 | +- **Framework:** Jasmine + Karma (NOT Jest). This project uses Karma + Jasmine. |
| 113 | +- **Always generate tests with new code:** for every new component, service, directive, pipe, store, guard, or resolver, create the co-located `*.spec.ts` file in the same edit. Do not deliver new production code without corresponding tests. |
| 114 | +- **Code coverage:** maintain >80% coverage for `projects/web`. New and modified code must include tests that cover main behavior and important edge cases so coverage stays above this target. |
| 115 | +- Test behavior, not implementation details. |
| 116 | +- Test file path alias: `@tests/*` → `projects/web/tests/*`. |
| 117 | +- See **angular-testing** skill for patterns (TestBed, mocking, HTTP testing, signal component tests). See **web-testing** rule for runner, coverage config, and project test utilities. |
| 118 | + |
| 119 | +## Routing & Lazy Loading |
| 120 | + |
| 121 | +- Lazy-load feature routes with `loadComponent` / `loadChildren`. |
| 122 | +- Heavy components can be lazy-loaded in templates with `@defer`. |
| 123 | +- Use functional route guards (`CanActivateFn`, `CanDeactivateFn`) for authentication and authorization. |
| 124 | +- Avoid direct DOM manipulation — use Angular's templating and renderer APIs instead. |
| 125 | +- See **angular-routing** skill. |
| 126 | + |
| 127 | +## Format and lint after editing |
| 128 | + |
| 129 | +- **After modifying any file under `projects/web`**, run format-check, then format only if needed, then lint. Use as **few files as possible** — only the files you actually changed. All commands from the **workspace root**; paths space-separated, relative to the workspace root. |
| 130 | +- **1. Check if format is required** (same file list as modified files): |
| 131 | + ```bash |
| 132 | + npx nx run web:format-check --files="projects/web/src/app/foo/foo.component.ts projects/web/src/app/foo/foo.component.html" |
| 133 | + ``` |
| 134 | + If this fails (exit non-zero), formatting is required for those files. |
| 135 | +- **2. Format** only when format-check indicated format is needed. Use the same file list: |
| 136 | + ```bash |
| 137 | + npx nx run web:format --files="projects/web/src/app/foo/foo.component.ts projects/web/src/app/foo/foo.component.html" |
| 138 | + ``` |
| 139 | + If you modified many files and listing them is impractical, use `npx nx run web:format-check` then `npx nx run web:format` (whole project). Prefer listing the specific files. |
| 140 | +- **3. Lint:** Always run lint with auto-fix after any format step: |
| 141 | + ```bash |
| 142 | + npx nx run web:lint --fix |
| 143 | + ``` |
| 144 | +- Summary: run `format-check --files="..."` for the modified files; if format is required, run `format --files="..."` with the same list; then always run `npx nx run web:lint --fix`. |
0 commit comments