@tungvivas/angular-vibe-kit 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +129 -0
- package/bin/cli.js +245 -0
- package/commands/dev-cycle.md +139 -0
- package/commands/init.md +103 -0
- package/commands/new-feature.md +69 -0
- package/commands/review-pr.md +63 -0
- package/commands/start.md +21 -0
- package/commands/update-status.md +19 -0
- package/commands/write-context.md +71 -0
- package/commands/write-tests.md +110 -0
- package/package.json +40 -0
- package/practices/v12-13.md +193 -0
- package/practices/v14-15.md +133 -0
- package/practices/v16.md +127 -0
- package/practices/v17.md +166 -0
- package/practices/v18-19.md +121 -0
- package/practices/v20plus.md +210 -0
- package/templates/CLAUDE.md +55 -0
- package/templates/docs/API_CONTRACT.md +46 -0
- package/templates/docs/ARCHITECTURE.md +27 -0
- package/templates/docs/DESIGN_SYSTEM.md +27 -0
- package/templates/docs/PROJECT-STATUS.md +19 -0
- package/templates/docs/decisions/001-state-management.md +18 -0
- package/templates/docs/decisions/002-auth-token-storage.md +19 -0
- package/templates/rules/project-rules.md +72 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# Angular Best Practices — v14 / v15 (NgModule Era)
|
|
2
|
+
|
|
3
|
+
> Framework-level patterns only. This file describes HOW to write idiomatic
|
|
4
|
+
> Angular 14/15 — syntax, DI, components, RxJS. It does NOT dictate folder
|
|
5
|
+
> structure, state-management choice, or naming — those are project decisions
|
|
6
|
+
> recorded in `docs/PROJECT-RULES.md` (inferred from the codebase).
|
|
7
|
+
>
|
|
8
|
+
> When the project already follows a correct pattern, follow the project.
|
|
9
|
+
> When it is below standard, apply this standard to NEW code only.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Module System
|
|
14
|
+
- **NgModule is the norm.** Each feature has a `*.module.ts` and a `*-routing.module.ts`.
|
|
15
|
+
- Standalone components exist (v14 preview, v15 stable) but the ecosystem and most projects are module-based — do not force standalone unless the project already uses it.
|
|
16
|
+
- Feature modules are lazy-loaded via `loadChildren`.
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
@NgModule({
|
|
20
|
+
declarations: [UserListComponent, UserCardComponent],
|
|
21
|
+
imports: [CommonModule, UserRoutingModule, ReactiveFormsModule],
|
|
22
|
+
})
|
|
23
|
+
export class UserModule {}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Component Pattern
|
|
27
|
+
- `changeDetection: ChangeDetectionStrategy.OnPush` on every component.
|
|
28
|
+
- Smart (container) vs dumb (presentational) split.
|
|
29
|
+
- Inputs/outputs with `@Input()` / `@Output()`.
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
@Component({
|
|
33
|
+
selector: 'app-user-list',
|
|
34
|
+
templateUrl: './user-list.component.html',
|
|
35
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
36
|
+
})
|
|
37
|
+
export class UserListComponent implements OnInit, OnDestroy {
|
|
38
|
+
users: User[] = [];
|
|
39
|
+
isLoading = false;
|
|
40
|
+
error: string | null = null;
|
|
41
|
+
private destroy$ = new Subject<void>();
|
|
42
|
+
|
|
43
|
+
constructor(private userService: UserService) {}
|
|
44
|
+
|
|
45
|
+
ngOnInit(): void { this.loadUsers(); }
|
|
46
|
+
ngOnDestroy(): void { this.destroy$.next(); this.destroy$.complete(); }
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Template Syntax — Structural Directives
|
|
51
|
+
- Use `*ngIf`, `*ngFor`, `[ngSwitch]`. (Built-in `@if/@for` does NOT exist yet.)
|
|
52
|
+
- Always use `trackBy` with `*ngFor` for lists.
|
|
53
|
+
|
|
54
|
+
```html
|
|
55
|
+
<app-spinner *ngIf="isLoading"></app-spinner>
|
|
56
|
+
<div *ngIf="error as msg" class="error">{{ msg }}</div>
|
|
57
|
+
<app-user-card
|
|
58
|
+
*ngFor="let user of users; trackBy: trackById"
|
|
59
|
+
[user]="user"
|
|
60
|
+
(delete)="onDelete($event)">
|
|
61
|
+
</app-user-card>
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Services & Dependency Injection
|
|
65
|
+
- **Constructor injection** — `inject()` is not yet idiomatic here.
|
|
66
|
+
- `@Injectable({ providedIn: 'root' })` for singletons.
|
|
67
|
+
- HttpClient lives ONLY in services. Services return `Observable<T>`, never subscribe internally.
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
@Injectable({ providedIn: 'root' })
|
|
71
|
+
export class UserService {
|
|
72
|
+
private baseUrl = `${environment.apiUrl}/users`;
|
|
73
|
+
constructor(private http: HttpClient) {}
|
|
74
|
+
|
|
75
|
+
getAll(params?: UserFilterParams): Observable<ApiResponse<User[]>> {
|
|
76
|
+
return this.http.get<ApiResponse<User[]>>(this.baseUrl, { params: { ...params } });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## HTTP
|
|
82
|
+
- `HttpClientModule` imported in `CoreModule` / `AppModule`.
|
|
83
|
+
- Class-based interceptors registered with `HTTP_INTERCEPTORS` multi-provider.
|
|
84
|
+
- Auth interceptor attaches `Authorization`; error interceptor maps errors.
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
@Injectable()
|
|
88
|
+
export class AuthInterceptor implements HttpInterceptor {
|
|
89
|
+
constructor(private auth: AuthService) {}
|
|
90
|
+
intercept(req: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
|
|
91
|
+
const token = this.auth.accessToken;
|
|
92
|
+
return token
|
|
93
|
+
? next.handle(req.clone({ setHeaders: { Authorization: `Bearer ${token}` } }))
|
|
94
|
+
: next.handle(req);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Routing
|
|
100
|
+
- Lazy load feature modules with `loadChildren: () => import('...').then(m => m.UserModule)`.
|
|
101
|
+
- Class-based guards implementing `CanActivate`.
|
|
102
|
+
|
|
103
|
+
## Reactive / RxJS
|
|
104
|
+
- No Signals. State is plain class fields + RxJS (`BehaviorSubject` for shared state).
|
|
105
|
+
- Tear down subscriptions with `takeUntil(this.destroy$)` and the OnDestroy pattern.
|
|
106
|
+
- Prefer the `async` pipe in templates over manual subscribe.
|
|
107
|
+
- Avoid `toPromise()` (deprecated) — use `firstValueFrom()` / `lastValueFrom()`.
|
|
108
|
+
|
|
109
|
+
## Forms
|
|
110
|
+
- Reactive Forms (`FormBuilder`) for validated forms. Template-driven only for trivial cases.
|
|
111
|
+
- Show validation messages when control is `touched` and invalid.
|
|
112
|
+
|
|
113
|
+
## TypeScript
|
|
114
|
+
- No `any`. Interfaces/types for models. Mirror backend DTOs in `*.model.ts`.
|
|
115
|
+
|
|
116
|
+
## Testing
|
|
117
|
+
- Unit (service): `HttpClientTestingModule` + `HttpTestingController`.
|
|
118
|
+
- Component: `TestBed.configureTestingModule` + mocked service.
|
|
119
|
+
- E2E: Protractor is legacy/EOL — prefer Cypress or Playwright.
|
|
120
|
+
- Runner: Karma/Jasmine by default; Jest if the project added it.
|
|
121
|
+
|
|
122
|
+
## Security
|
|
123
|
+
- Tokens in memory or httpOnly cookie — NOT localStorage.
|
|
124
|
+
- No hardcoded API URL — use `environment`.
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## Notes vs Newer Versions
|
|
129
|
+
- No `@if/@for` control flow — use `*ngIf/*ngFor`.
|
|
130
|
+
- No `inject()` idiom — use constructor injection.
|
|
131
|
+
- No Signals — use RxJS + `BehaviorSubject`.
|
|
132
|
+
- No `@defer` — lazy-load at the route/module level instead.
|
|
133
|
+
- No `takeUntilDestroyed` — use the `Subject` + `takeUntil` + `ngOnDestroy` pattern.
|
package/practices/v16.md
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# Angular Best Practices — v16 (Signals Preview Era)
|
|
2
|
+
|
|
3
|
+
> Framework-level patterns only. This file describes HOW to write idiomatic
|
|
4
|
+
> Angular 16 — syntax, DI, components, RxJS/Signals. It does NOT dictate folder
|
|
5
|
+
> structure, state-management choice, or naming — those are project decisions
|
|
6
|
+
> recorded in `docs/PROJECT-RULES.md` (inferred from the codebase).
|
|
7
|
+
>
|
|
8
|
+
> When the project already follows a correct pattern, follow the project.
|
|
9
|
+
> When it is below standard, apply this standard to NEW code only.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Module System
|
|
14
|
+
- **Transitional release.** Standalone is stable and recommended for new code, but many v16 projects are still NgModule-based.
|
|
15
|
+
- If the project is standalone → use `bootstrapApplication` + standalone components.
|
|
16
|
+
- If the project is module-based → keep adding to modules; introduce standalone only where it does not conflict.
|
|
17
|
+
|
|
18
|
+
## Component Pattern
|
|
19
|
+
- `changeDetection: ChangeDetectionStrategy.OnPush` on every component.
|
|
20
|
+
- Smart vs dumb split.
|
|
21
|
+
- **Required inputs** available: `@Input({ required: true }) user!: User;`.
|
|
22
|
+
- `DestroyRef` + `takeUntilDestroyed()` introduced — use them for teardown.
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
@Component({
|
|
26
|
+
selector: 'app-user-list',
|
|
27
|
+
standalone: true,
|
|
28
|
+
imports: [CommonModule, UserCardComponent],
|
|
29
|
+
templateUrl: './user-list.component.html',
|
|
30
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
31
|
+
})
|
|
32
|
+
export class UserListComponent {
|
|
33
|
+
private userService = inject(UserService);
|
|
34
|
+
private destroyRef = inject(DestroyRef);
|
|
35
|
+
|
|
36
|
+
// Signals are in developer preview in v16 — fine to use for local state.
|
|
37
|
+
users = signal<User[]>([]);
|
|
38
|
+
isLoading = signal(false);
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Template Syntax — Structural Directives
|
|
43
|
+
- Use `*ngIf`, `*ngFor` (+ `trackBy`), `[ngSwitch]`. The `@if/@for` built-in control flow does NOT exist yet (that is v17).
|
|
44
|
+
|
|
45
|
+
```html
|
|
46
|
+
<app-spinner *ngIf="isLoading()"></app-spinner>
|
|
47
|
+
<app-user-card
|
|
48
|
+
*ngFor="let user of users(); trackBy: trackById"
|
|
49
|
+
[user]="user"
|
|
50
|
+
(delete)="onDelete($event)">
|
|
51
|
+
</app-user-card>
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Services & Dependency Injection
|
|
55
|
+
- **`inject()` function is idiomatic in v16** — prefer it over constructor injection for new code.
|
|
56
|
+
- `@Injectable({ providedIn: 'root' })` for singletons.
|
|
57
|
+
- HttpClient only in services; services return `Observable<T>` and never subscribe.
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
@Injectable({ providedIn: 'root' })
|
|
61
|
+
export class UserService {
|
|
62
|
+
private http = inject(HttpClient);
|
|
63
|
+
private baseUrl = `${environment.apiUrl}/users`;
|
|
64
|
+
|
|
65
|
+
getAll(params?: UserFilterParams): Observable<ApiResponse<User[]>> {
|
|
66
|
+
return this.http.get<ApiResponse<User[]>>(this.baseUrl, { params: { ...params } });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## HTTP
|
|
72
|
+
- `provideHttpClient(withInterceptors([...]))` available for standalone apps (functional interceptors).
|
|
73
|
+
- Module-based apps still use `HttpClientModule` + class interceptors via `HTTP_INTERCEPTORS`.
|
|
74
|
+
- Avoid `toPromise()` — use `firstValueFrom()`.
|
|
75
|
+
|
|
76
|
+
## Routing
|
|
77
|
+
- Lazy load with `loadComponent()` (standalone) or `loadChildren()` (modules).
|
|
78
|
+
- Functional guards (`CanActivateFn`) are available and preferred for new code.
|
|
79
|
+
|
|
80
|
+
## Reactive / RxJS / Signals
|
|
81
|
+
- **Signals are developer preview** — safe for local component state; teams may still prefer RxJS for shared state.
|
|
82
|
+
- `computed()` for derived state. `effect()` for side effects.
|
|
83
|
+
- Subscriptions: `takeUntilDestroyed(this.destroyRef)`.
|
|
84
|
+
- Prefer the `async` pipe over manual subscribe.
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
private loadUsers(): void {
|
|
88
|
+
this.isLoading.set(true);
|
|
89
|
+
this.userService.getAll()
|
|
90
|
+
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
91
|
+
.subscribe({
|
|
92
|
+
next: (res) => { this.users.set(res.data); this.isLoading.set(false); },
|
|
93
|
+
error: () => { this.isLoading.set(false); },
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Forms
|
|
99
|
+
- Reactive Forms (`FormBuilder` / `NonNullableFormBuilder`) for validated forms.
|
|
100
|
+
- Show validation messages when control is `touched` and invalid.
|
|
101
|
+
|
|
102
|
+
## TypeScript
|
|
103
|
+
- No `any`. Interfaces/types for models, mirror backend DTOs in `*.model.ts`.
|
|
104
|
+
|
|
105
|
+
## Testing
|
|
106
|
+
- Unit (service): `HttpClientTestingModule` + `HttpTestingController`.
|
|
107
|
+
- Component: `TestBed` + mocked service.
|
|
108
|
+
- E2E: Cypress or Playwright.
|
|
109
|
+
- Runner: Karma/Jasmine by default; Jest if the project added it.
|
|
110
|
+
|
|
111
|
+
## Security
|
|
112
|
+
- Tokens in memory or httpOnly cookie — NOT localStorage.
|
|
113
|
+
- No hardcoded API URL — use `environment`.
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## Breaking Changes / Notes vs v14-15
|
|
118
|
+
- **`inject()`** is now idiomatic (over constructor injection).
|
|
119
|
+
- **`DestroyRef` + `takeUntilDestroyed()`** replace the `Subject + takeUntil + ngOnDestroy` boilerplate.
|
|
120
|
+
- **Required inputs** `@Input({ required: true })`.
|
|
121
|
+
- **Signals** available (developer preview) for local state.
|
|
122
|
+
- **Functional interceptors/guards** available for standalone apps.
|
|
123
|
+
|
|
124
|
+
## Notes vs v17
|
|
125
|
+
- No `@if/@for/@switch` built-in control flow yet — still `*ngIf/*ngFor`.
|
|
126
|
+
- No `@defer` blocks.
|
|
127
|
+
- Standalone not yet the CLI default.
|
package/practices/v17.md
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# Angular Best Practices — v17 (Modern Era)
|
|
2
|
+
|
|
3
|
+
> Framework-level patterns only. This file describes HOW to write idiomatic
|
|
4
|
+
> Angular 17 — syntax, DI, components, RxJS. It does NOT dictate folder
|
|
5
|
+
> structure, state-management choice, or naming — those are project decisions
|
|
6
|
+
> recorded in `docs/PROJECT-RULES.md` (inferred from the codebase).
|
|
7
|
+
>
|
|
8
|
+
> Read this together with the project's own rules. When the project already
|
|
9
|
+
> follows a correct pattern, follow the project. When the project is below
|
|
10
|
+
> standard, apply this standard to NEW code only (see Coexistence in PROJECT-RULES.md).
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Module System
|
|
15
|
+
- **Standalone components by default** — `standalone: true`. No NgModule for new code.
|
|
16
|
+
- Bootstrap via `bootstrapApplication(AppComponent, appConfig)` in `main.ts`.
|
|
17
|
+
- Providers configured in `app.config.ts` (`provideRouter`, `provideHttpClient`, etc.).
|
|
18
|
+
- NgModule still supported — if the project is module-based, do not migrate; add new features as standalone where it does not conflict.
|
|
19
|
+
|
|
20
|
+
## Component Pattern
|
|
21
|
+
- `changeDetection: ChangeDetectionStrategy.OnPush` on every component.
|
|
22
|
+
- Smart (page/container) components own data + side effects; presentational (dumb) components only render and emit.
|
|
23
|
+
- Keep templates in separate `.html` files for non-trivial components.
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
@Component({
|
|
27
|
+
selector: 'app-user-list',
|
|
28
|
+
standalone: true,
|
|
29
|
+
imports: [UserCardComponent, MatPaginatorModule],
|
|
30
|
+
templateUrl: './user-list.component.html',
|
|
31
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
32
|
+
})
|
|
33
|
+
export class UserListComponent {
|
|
34
|
+
private userService = inject(UserService);
|
|
35
|
+
private destroyRef = inject(DestroyRef);
|
|
36
|
+
|
|
37
|
+
users = signal<User[]>([]);
|
|
38
|
+
isLoading = signal(false);
|
|
39
|
+
error = signal<string | null>(null);
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Template Syntax — Built-in Control Flow (new in v17)
|
|
44
|
+
- Use `@if`, `@else`, `@for`, `@switch` — the new built-in control flow.
|
|
45
|
+
- `@for` **requires** `track`: `@for (user of users(); track user.id) { ... }`.
|
|
46
|
+
- Prefer `@defer` for heavy/below-the-fold blocks.
|
|
47
|
+
|
|
48
|
+
```html
|
|
49
|
+
@if (isLoading()) {
|
|
50
|
+
<app-spinner />
|
|
51
|
+
} @else if (error()) {
|
|
52
|
+
<app-error [message]="error()!" />
|
|
53
|
+
} @else {
|
|
54
|
+
@for (user of users(); track user.id) {
|
|
55
|
+
<app-user-card [user]="user" (delete)="onDelete($event)" />
|
|
56
|
+
} @empty {
|
|
57
|
+
<app-empty-state message="No users yet" />
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
- `*ngIf` / `*ngFor` still work but prefer the new syntax for new code. Do not mass-migrate legacy templates.
|
|
63
|
+
|
|
64
|
+
## Services & Dependency Injection
|
|
65
|
+
- Use the `inject()` function instead of constructor injection for new code.
|
|
66
|
+
- `@Injectable({ providedIn: 'root' })` for singleton services.
|
|
67
|
+
- HttpClient lives ONLY in services — never call HTTP from a component.
|
|
68
|
+
- Services return `Observable<T>` — never subscribe inside a service.
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
@Injectable({ providedIn: 'root' })
|
|
72
|
+
export class UserService {
|
|
73
|
+
private http = inject(HttpClient);
|
|
74
|
+
private baseUrl = `${environment.apiUrl}/users`;
|
|
75
|
+
|
|
76
|
+
getAll(params?: UserFilterParams): Observable<ApiResponse<User[]>> {
|
|
77
|
+
return this.http.get<ApiResponse<User[]>>(this.baseUrl, { params: { ...params } });
|
|
78
|
+
}
|
|
79
|
+
getById(id: number): Observable<ApiResponse<User>> {
|
|
80
|
+
return this.http.get<ApiResponse<User>>(`${this.baseUrl}/${id}`);
|
|
81
|
+
}
|
|
82
|
+
create(payload: CreateUserRequest): Observable<ApiResponse<User>> {
|
|
83
|
+
return this.http.post<ApiResponse<User>>(this.baseUrl, payload);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## HTTP
|
|
89
|
+
- Configure with `provideHttpClient(withInterceptors([...]))` — functional interceptors.
|
|
90
|
+
- Auth: a functional interceptor attaches the `Authorization` header.
|
|
91
|
+
- Errors: an error interceptor maps HTTP errors to user-facing messages; components show them.
|
|
92
|
+
- Never use `toPromise()` (removed) — use `firstValueFrom()` if a promise is truly needed.
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
export const authInterceptor: HttpInterceptorFn = (req, next) => {
|
|
96
|
+
const token = inject(AuthService).accessToken();
|
|
97
|
+
return token
|
|
98
|
+
? next(req.clone({ setHeaders: { Authorization: `Bearer ${token}` } }))
|
|
99
|
+
: next(req);
|
|
100
|
+
};
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Routing
|
|
104
|
+
- Lazy-load every feature route with `loadComponent()` / `loadChildren()`.
|
|
105
|
+
- Functional guards: `CanActivateFn`. Functional resolvers: `ResolveFn`.
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
export const routes: Routes = [
|
|
109
|
+
{ path: 'login', loadComponent: () => import('./features/auth/pages/login/login.component').then(m => m.LoginComponent) },
|
|
110
|
+
{
|
|
111
|
+
path: '',
|
|
112
|
+
canActivate: [authGuard],
|
|
113
|
+
children: [
|
|
114
|
+
{ path: 'users', loadComponent: () => import('./features/user/pages/user-list/user-list.component').then(m => m.UserListComponent) },
|
|
115
|
+
],
|
|
116
|
+
},
|
|
117
|
+
];
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Reactive / RxJS / Signals
|
|
121
|
+
- **Signals** for component state (`signal`, `computed`, `effect`). `computed()` for derived state — never duplicate state.
|
|
122
|
+
- For subscriptions, always tear down with `takeUntilDestroyed(destroyRef)`.
|
|
123
|
+
- Prefer the `async` pipe in templates over manual `subscribe`.
|
|
124
|
+
- `toSignal()` / `toObservable()` bridge RxJS and signals when needed.
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
private loadUsers(): void {
|
|
128
|
+
this.isLoading.set(true);
|
|
129
|
+
this.userService.getAll()
|
|
130
|
+
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
131
|
+
.subscribe({
|
|
132
|
+
next: (res) => { this.users.set(res.data); this.isLoading.set(false); },
|
|
133
|
+
error: () => { this.error.set('Could not load users'); this.isLoading.set(false); },
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Forms
|
|
139
|
+
- Reactive Forms (`FormBuilder` / `NonNullableFormBuilder`) for anything with validation.
|
|
140
|
+
- Template-driven only for 1–2 trivial fields.
|
|
141
|
+
- Show validation messages when a control is `touched` and invalid.
|
|
142
|
+
|
|
143
|
+
## TypeScript
|
|
144
|
+
- No `any`. Use `interface`/`type`, `unknown` + narrowing for untrusted input.
|
|
145
|
+
- Mirror backend DTOs in `*.model.ts` (e.g. `ApiResponse<T>`, `PaginationResponse<T>`).
|
|
146
|
+
- `readonly` for inputs that never change.
|
|
147
|
+
|
|
148
|
+
## Testing
|
|
149
|
+
- Unit (service): `HttpClientTestingModule` + `HttpTestingController`. Test success + error + empty per method.
|
|
150
|
+
- Component: `TestBed` + mocked service (`jasmine.createSpyObj` or `jest.fn()`). Test render, interaction, loading, error states.
|
|
151
|
+
- E2E: Playwright for critical flows (login, CRUD).
|
|
152
|
+
- Runner: Jest or Karma/Jasmine — follow whatever the project already uses.
|
|
153
|
+
|
|
154
|
+
## Security
|
|
155
|
+
- Tokens in memory or httpOnly cookie — NOT localStorage.
|
|
156
|
+
- Never hardcode the API URL — use `environment`.
|
|
157
|
+
- Guard routes that require auth.
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## Breaking Changes vs v16
|
|
162
|
+
- **Built-in control flow** `@if/@for/@switch` introduced (stable). `@for` requires `track`.
|
|
163
|
+
- **`@defer`** blocks introduced for lazy view loading.
|
|
164
|
+
- Standalone is the default in the CLI scaffolding.
|
|
165
|
+
- Vite + esbuild is the default dev/build pipeline.
|
|
166
|
+
- `toPromise()` fully removed — use `firstValueFrom()`.
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# Angular Best Practices — v18 / v19 (Signal-First Era)
|
|
2
|
+
|
|
3
|
+
> Framework-level patterns only. This file describes HOW to write idiomatic
|
|
4
|
+
> Angular 18/19 — syntax, DI, components, Signals. It does NOT dictate folder
|
|
5
|
+
> structure, state-management choice, or naming — those are project decisions
|
|
6
|
+
> recorded in `docs/PROJECT-RULES.md` (inferred from the codebase).
|
|
7
|
+
>
|
|
8
|
+
> When the project already follows a correct pattern, follow the project.
|
|
9
|
+
> When it is below standard, apply this standard to NEW code only.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Module System
|
|
14
|
+
- **Standalone everywhere.** In v19 standalone is the implicit default (`standalone: true` no longer needs to be written, though being explicit is fine).
|
|
15
|
+
- Bootstrap with `bootstrapApplication(AppComponent, appConfig)`.
|
|
16
|
+
- Providers in `app.config.ts`. Do not add NgModules for new code.
|
|
17
|
+
|
|
18
|
+
## Component Pattern
|
|
19
|
+
- `ChangeDetectionStrategy.OnPush` everywhere (or run zoneless — see below).
|
|
20
|
+
- **Signal-based inputs/outputs** are idiomatic:
|
|
21
|
+
- `input()` / `input.required()` instead of `@Input()`.
|
|
22
|
+
- `output()` instead of `@Output()`.
|
|
23
|
+
- `model()` for two-way binding.
|
|
24
|
+
- `viewChild()` / `contentChild()` signal queries.
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
@Component({
|
|
28
|
+
selector: 'app-user-card',
|
|
29
|
+
imports: [],
|
|
30
|
+
templateUrl: './user-card.component.html',
|
|
31
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
32
|
+
})
|
|
33
|
+
export class UserCardComponent {
|
|
34
|
+
user = input.required<User>();
|
|
35
|
+
delete = output<number>();
|
|
36
|
+
onDeleteClick(): void { this.delete.emit(this.user().id); }
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Template Syntax — Built-in Control Flow
|
|
41
|
+
- `@if`, `@else`, `@for` (with required `track`), `@switch`, `@defer`, `@empty`, `@placeholder`.
|
|
42
|
+
- These are the default and `*ngIf/*ngFor` are effectively legacy — use the block syntax for all new templates.
|
|
43
|
+
|
|
44
|
+
```html
|
|
45
|
+
@if (isLoading()) {
|
|
46
|
+
<app-spinner />
|
|
47
|
+
} @else {
|
|
48
|
+
@for (user of users(); track user.id) {
|
|
49
|
+
<app-user-card [user]="user" (delete)="onDelete($event)" />
|
|
50
|
+
} @empty {
|
|
51
|
+
<app-empty-state message="No users yet" />
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Services & Dependency Injection
|
|
57
|
+
- `inject()` everywhere. Constructor injection is legacy.
|
|
58
|
+
- `@Injectable({ providedIn: 'root' })` for singletons.
|
|
59
|
+
- HttpClient only in services; services return `Observable<T>` and never subscribe.
|
|
60
|
+
|
|
61
|
+
## HTTP
|
|
62
|
+
- `provideHttpClient(withInterceptors([...]))` with functional interceptors.
|
|
63
|
+
- `httpResource()` (v19+, experimental) for declarative resource fetching where it fits — otherwise plain HttpClient.
|
|
64
|
+
- Never use `toPromise()` — use `firstValueFrom()`.
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
export const authInterceptor: HttpInterceptorFn = (req, next) => {
|
|
68
|
+
const token = inject(AuthService).accessToken();
|
|
69
|
+
return token
|
|
70
|
+
? next(req.clone({ setHeaders: { Authorization: `Bearer ${token}` } }))
|
|
71
|
+
: next(req);
|
|
72
|
+
};
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Routing
|
|
76
|
+
- Lazy-load with `loadComponent()` / `loadChildren()`.
|
|
77
|
+
- Functional guards (`CanActivateFn`) and resolvers (`ResolveFn`).
|
|
78
|
+
|
|
79
|
+
## Reactive / Signals (signal-first)
|
|
80
|
+
- **Signals are the primary state primitive.** `signal`, `computed`, `effect`, `linkedSignal` (v19), `resource()` / `rxResource()` (v19, experimental).
|
|
81
|
+
- Bridge with `toSignal()` / `toObservable()`.
|
|
82
|
+
- Subscriptions (when still needed): `takeUntilDestroyed(destroyRef)`.
|
|
83
|
+
- Prefer the `async` pipe / signal reads in templates over manual subscribe.
|
|
84
|
+
- **Zoneless** change detection is available (experimental via `provideExperimentalZonelessChangeDetection()`). If the project enables it, do NOT rely on `setTimeout`/zone-triggered CD — drive updates through signals.
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
export class UserListComponent {
|
|
88
|
+
private userService = inject(UserService);
|
|
89
|
+
private destroyRef = inject(DestroyRef);
|
|
90
|
+
|
|
91
|
+
users = signal<User[]>([]);
|
|
92
|
+
isLoading = signal(false);
|
|
93
|
+
userCount = computed(() => this.users().length);
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Forms
|
|
98
|
+
- Reactive Forms (`NonNullableFormBuilder`) for validated forms.
|
|
99
|
+
- Show validation messages when a control is `touched` and invalid.
|
|
100
|
+
|
|
101
|
+
## TypeScript
|
|
102
|
+
- No `any`. Interfaces/types for models, mirror backend DTOs in `*.model.ts`. `readonly` where applicable.
|
|
103
|
+
|
|
104
|
+
## Testing
|
|
105
|
+
- Unit (service): `HttpClientTestingModule` + `HttpTestingController` (or `provideHttpClientTesting()`).
|
|
106
|
+
- Component: `TestBed` + mocked service; assert signal values and rendered output.
|
|
107
|
+
- E2E: Playwright.
|
|
108
|
+
- Runner: Jest or Karma/Jasmine — follow the project. (Web Test Runner / Vitest also appear in newer setups.)
|
|
109
|
+
|
|
110
|
+
## Security
|
|
111
|
+
- Tokens in memory or httpOnly cookie — NOT localStorage.
|
|
112
|
+
- No hardcoded API URL — use `environment`.
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## Breaking Changes / Notes vs v17
|
|
117
|
+
- **Signal inputs/outputs**: `input()`, `input.required()`, `output()`, `model()`, signal queries (`viewChild()`/`contentChild()`). Developer preview in v17.1–v18; **production-ready as of v19**. Preferred over decorators for new code on v19; on v18 confirm the team accepts the preview API.
|
|
118
|
+
- **`linkedSignal()`** (introduced v19, experimental) and **`resource()` / `rxResource()` / `httpResource()`** (experimental — `httpResource` from v19.2) for derived & async state. Treat all as experimental on v18-19.
|
|
119
|
+
- **Zoneless** change detection available but **experimental** — `provideExperimentalZonelessChangeDetection()` (becomes stable `provideZonelessChangeDetection()` in v20).
|
|
120
|
+
- **Standalone is the implicit default since v19** (`standalone: true` optional). On v18 and earlier `standalone` defaults to `false` — must be set explicitly.
|
|
121
|
+
- Event replay / incremental hydration (`@defer`-powered) is **developer preview** here (graduates to stable in v20) — relevant only with SSR.
|