| name | dev-angular-guide |
| description | Développement d'applications Angular avec modules, composants, services, RxJS, routing et formulaires réactifs. Se déclenche avec "Angular", "NgModule", "RxJS", "Angular CLI", "composant Angular", "service Angular". Also triggers on "Angular app", "RxJS observables", "Angular reactive forms". |
Guide Angular (v17+)
1. Initialisation du projet
npm install -g @angular/cli
ng new my-app --routing --style=scss --standalone
cd my-app && ng serve
Critère de décision — standalone vs NgModule
| Situation | Recommandation |
|---|
| Nouveau projet (v17+) | Standalone components (défaut CLI) |
| Migration progressive | Hybrid : ng generate component --standalone |
| Legacy codebase | Maintenir NgModules, migrer par feature |
Structure cible pour un projet moyen :
src/app/
core/ # services singleton, interceptors, guards globaux
shared/ # composants, pipes, directives réutilisables
features/
orders/ # composant, service, model, route propres à la feature
users/
app.routes.ts
app.config.ts
2. Composants standalone et Signals
import { Component, signal, computed, effect } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
standalone: true,
imports: [CommonModule],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<p>Count: {{ count() }}</p>
<p>Double: {{ double() }}</p>
<button (click)="increment()">+1</button>
`,
})
export class SignalCounterComponent {
count = signal(0);
double = computed(() => this.count() * 2);
constructor() {
effect(() => console.log('count changed:', this.count()));
}
increment() { this.count.update(c => c + 1); }
}
Règle : ChangeDetectionStrategy.OnPush est obligatoire. Avec les signals, la détection est automatiquement granulaire — pas besoin de markForCheck().
Input typés (v17.1+)
readonly userId = input.required<number>();
readonly label = input<string>('default');
readonly selected = output<User>();
3. Services et injection de dépendances
@Injectable({ providedIn: 'root' })
export class UserService {
private http = inject(HttpClient);
private cache = new Map<number, User>();
getUser(id: number): Observable<User> {
if (this.cache.has(id)) return of(this.cache.get(id)!);
return this.http.get<User>(`/api/users/${id}`).pipe(
tap(u => this.cache.set(id, u)),
catchError(err => { throw new UserError(err); })
);
}
}
inject() vs constructor DI : Préfère inject() dans les standalone components et les fonctions (guards, resolvers). Le constructeur reste valide dans les services.
4. RxJS — opérateurs essentiels
search$ = this.searchControl.valueChanges.pipe(
debounceTime(300),
distinctUntilChanged(),
filter(q => q.length > 2),
switchMap(q => this.api.search(q)),
takeUntilDestroyed(),
);
data$ = combineLatest({
users: this.userService.getAll(),
roles: this.roleService.getAll(),
}).pipe(
map(({ users, roles }) => users.map(u => ({
...u, roleName: roles.find(r => r.id === u.roleId)?.name
})))
);
Choisir le bon opérateur de flatten :
| Cas d'usage | Opérateur |
|---|
| Requête HTTP (annuler la précédente) | switchMap |
| Upload parallèle | mergeMap |
| Requêtes en séquence | concatMap |
| Ignorer si déjà en cours | exhaustMap |
Éviter les memory leaks :
readonly users = toSignal(this.userService.getAll(), { initialValue: [] });
5. Routing avec lazy loading
export const routes: Routes = [
{ path: '', redirectTo: 'dashboard', pathMatch: 'full' },
{
path: 'orders',
loadComponent: () => import('./features/orders/orders.component')
.then(m => m.OrdersComponent),
canActivate: [authGuard],
resolve: { orders: ordersResolver },
},
{
path: 'admin',
loadChildren: () => import('./features/admin/admin.routes')
.then(m => m.ADMIN_ROUTES),
canMatch: [adminGuard],
},
];
Guard fonctionnel (v15+) :
export const authGuard: CanActivateFn = (route, state) => {
const auth = inject(AuthService);
const router = inject(Router);
return auth.isLoggedIn()
? true
: router.createUrlTree(['/login'], { queryParams: { returnUrl: state.url } });
};
Resolver typé :
export const ordersResolver: ResolveFn<Order[]> = () =>
inject(OrderService).getAll();
6. Formulaires réactifs
@Component({ standalone: true, imports: [ReactiveFormsModule] })
export class UserFormComponent {
private fb = inject(FormBuilder);
form = this.fb.group({
name: ['', [Validators.required, Validators.minLength(3)]],
email: ['', [Validators.required, Validators.email], [this.uniqueEmailValidator()]],
address: this.fb.group({
city: ['', Validators.required],
}),
});
private uniqueEmailValidator(): AsyncValidatorFn {
return ctrl => this.userService.checkEmail(ctrl.value).pipe(
map( => taken ? { : } : ),
( ()),
);
}
() {
(..) ;
..(..()).();
}
}
Template affichage d'erreur :
<input formControlName="name" />
@if (form.get('name')?.errors?.['required'] && form.get('name')?.touched) {
<span class="error">Champ requis</span>
}
7. Interceptors HTTP
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const token = inject(AuthService).getToken();
const authReq = token
? req.clone({ setHeaders: { Authorization: `Bearer ${token}` } })
: req;
return next(authReq).pipe(
catchError(err => {
if (err.status === 401) inject(AuthService).logout();
return throwError(() => err);
})
);
};
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(withInterceptors([authInterceptor])),
provideRouter(routes, withPreloading(PreloadAllModules)),
],
};
8. Tests
it('getUser retourne depuis cache', () => {
const { service } = TestBed.inject(UserService);
});
beforeEach(() => TestBed.configureTestingModule({
imports: [SignalCounterComponent],
}));
it('incrémenter le compteur', () => {
const fixture = TestBed.createComponent(SignalCounterComponent);
fixture.detectChanges();
fixture.nativeElement.querySelector('button').click();
fixture.detectChanges();
expect(fixture.nativeElement.querySelector('p').textContent).toContain('1');
});
Commandes :
ng test --code-coverage
jest --coverage --watchAll=false
Anti-patterns et pièges
| Piège | Correction |
|---|
Souscrire manuellement dans le composant (subscribe() sans cleanup) | async pipe, toSignal(), ou takeUntilDestroyed() |
| Logique métier dans le template | Déplacer dans un computed() ou un service |
any pour les réponses HTTP | Typer avec this.http.get<User[]>(...) |
Guard via classe (CanActivate) | Guard fonctionnel (CanActivateFn) — classes dépréciées v15+ |
NgModule imports inutiles dans un standalone | Importer uniquement ce qui est utilisé dans le composant |
ngOnDestroy + Subject/takeUntil | Remplacer par takeUntilDestroyed() (plus simple, moins de code) |
Appeler une méthode dans le template {{ getLabel() }} | Utiliser computed() ou pipe pur pour éviter les re-exécutions |
ChangeDetectionStrategy.Default | Toujours OnPush — meilleure performance, détection contrôlée |
Commandes CLI fréquentes
ng generate component features/orders/order-list --standalone --skip-tests=false
ng generate service core/auth --skip-tests=false
ng generate guard core/auth --functional
ng generate pipe shared/truncate --standalone
ng build --configuration production
ng update @angular/core @angular/cli
npx nx affected:test
Communication Rules — MANDATORY
- Ultra-concise. No filler, no preamble, no pleasantries.
- Never say "happy to help", "sure!", "great question", "let me", or similar.
- Tool first, talk second. Act before explaining.
- Result first. Lead with outcome, not process.
- Stop when done. No summary, no recap, no trailing commentary.
- No politeness wrappers. Direct and blunt.
- Minimum words. If one word works, do not use ten.
- No unsolicited explanations.
- No emoji unless asked.