Composants & Décorateurs
FondamentauxUn composant Angular est une classe TypeScript décorée par @Component. Chaque composant lie un template, un style et une logique. Depuis Angular 14+, le mode standalone simplifie tout.
standalone: true (Angular 14+) supprime le besoin de NgModules. Le composant s'auto-suffit.
@Input() : Reçoit des données du parent.
@Output() : Émet des événements vers le parent via
EventEmitter.Convention : Un fichier par composant, nom en kebab-case, classe en PascalCase.
// user-card.component.ts
import { Component, Input, Output, EventEmitter } from '@angular/core';
@Component({
selector: 'app-user-card',
standalone: true,
template: `
<div class="card">
<img [src]="avatar" [alt]="name" />
<h3>{{ name }}</h3>
<span class="badge">{{ role }}</span>
<button (click)="onSelect.emit(name)">Sélectionner</button>
</div>
`,
styles: [`
.card {
background: rgba(255,255,255,0.05);
border-radius: 12px;
padding: 16px;
}
`]
})
export class UserCardComponent {
@Input() name = '';
@Input() role = 'Dev';
@Input() avatar = '';
@Output() onSelect = new EventEmitter<string>();
}transform (v16+) : Transforme automatiquement la valeur reçue.
model() (v17+) : Two-way binding simplifié.
import { Component, Input, numberAttribute, booleanAttribute, model } from '@angular/core';
@Component({
selector: 'app-price-card',
standalone: true,
template: `
<div [class.highlighted]="highlighted">
<h3>{{ symbol }}</h3>
<span>{{ price | currency }}</span>
</div>
`
})
export class PriceCardComponent {
@Input({ required: true }) symbol!: string;
@Input({ transform: numberAttribute }) price = 0;
@Input({ transform: booleanAttribute }) highlighted = false;
value = model(0);
}select : Slots nommés pour projeter du contenu à des endroits spécifiques.
@ContentChild : Accède à un élément projeté depuis le composant enfant.
@Component({
selector: 'app-card-layout',
standalone: true,
template: `
<div class="card">
<header>
<ng-content select="[card-header]"></ng-content>
</header>
<main>
<ng-content></ng-content>
</main>
<footer>
<ng-content select="[card-footer]"></ng-content>
</footer>
</div>
`
})
export class CardLayoutComponent {}Signals & État réactif
FondamentauxDepuis Angular 16, les Signals offrent une réactivité granulaire sans Zone.js. Plus performant et prévisible que le change detection classique.
computed() : Valeur dérivée recalculée automatiquement.
effect() : Exécute un side-effect quand un signal change.
import { Component, signal, computed, effect } from '@angular/core';
@Component({
selector: 'app-counter',
standalone: true,
template: `
<p>Compteur : {{ count() }}</p>
<p>Double : {{ doubled() }}</p>
<button (click)="increment()">+1</button>
<button (click)="count.set(0)">Reset</button>
`
})
export class CounterComponent {
count = signal(0);
doubled = computed(() => this.count() * 2);
increment() {
this.count.update(c => c + 1);
}
constructor() {
effect(() => {
console.log('Count:', this.count());
});
}
}resource : Charge des données asynchrones liées à un signal.
import { signal, linkedSignal, resource } from '@angular/core';
const items = signal(['BTC', 'ETH', 'SOL']);
const selected = linkedSignal({
source: items,
computation: (list) => list[0]
});
const coinId = signal('bitcoin');
const coinData = resource({
request: coinId,
loader: async ({ request: id }) => {
const res = await fetch(`https://api.coingecko.com/api/v3/coins/${id}`);
return res.json();
}
});Templates & Directives
FondamentauxNouvelle syntaxe @if, @for, @switch depuis Angular 17. Plus lisible que *ngIf et *ngFor.
*ngIf.@for / @empty : Remplacement de
*ngFor.@switch : Plus lisible que l'ancien système de directives.
@if (isLoggedIn()) {
<h2>Bienvenue {{ userName() }}</h2>
} @else {
<p>Connectez-vous</p>
}
@for (coin of filteredCoins(); track coin.id) {
<div class="row">
<span>{{ coin.name }}</span>
<span>{{ coin.price | currency:'USD' }}</span>
</div>
} @empty {
<p>Aucun résultat</p>
}@placeholder : Affiché avant le chargement.
@loading : Pendant le chargement.
@error : Si le chargement échoue.
@defer (on viewport) {
<app-heavy-chart [data]="chartData()" />
} @placeholder {
<div class="skeleton">Graphique...</div>
} @loading (minimum 300ms) {
<app-spinner />
} @error {
<p>Erreur de chargement</p>
}Services & Dependency Injection
ArchitectureLes services encapsulent la logique métier et l'état partagé. Angular les injecte automatiquement via son système de DI.
providedIn: 'root' : Singleton global.
inject() : Méthode moderne pour récupérer un service.
import { Injectable, inject, signal } from '@angular/core';
import { HttpClient } from '@angular/common/http';
@Injectable({ providedIn: 'root' })
export class CryptoService {
private http = inject(HttpClient);
coins = signal([]);
loading = signal(false);
fetchCoins() {
this.loading.set(true);
this.http.get('/api/coins').subscribe({
next: (data) => {
this.coins.set(data);
this.loading.set(false);
},
error: (err) => {
console.error(err);
this.loading.set(false);
}
});
}
}Component : Nouvelle instance par composant.
InjectionToken : Pour injecter des valeurs non-classes.
import { InjectionToken, inject } from '@angular/core';
export const API_URL = new InjectionToken('API_URL');
bootstrapApplication(AppComponent, {
providers: [
{ provide: API_URL, useValue: 'https://api.example.com' }
]
});
@Component({
providers: [CartService]
})
export class ShopComponent {
private cart = inject(CartService);
private apiUrl = inject(API_URL);
}Routing & Navigation
ArchitectureRouteur intégré avec lazy loading, guards fonctionnels et paramètres dynamiques.
:id : Paramètre dynamique.
canActivate : Protège l'accès.
export const routes = [
{ path: '', component: HomeComponent },
{
path: 'dashboard',
loadComponent: () =>
import('./dashboard/dashboard.component')
.then(m => m.DashboardComponent),
canActivate: [authGuard]
},
{
path: 'coin/:id',
loadComponent: () =>
import('./coin-detail/coin-detail.component')
.then(m => m.CoinDetailComponent)
},
{ path: '**', component: NotFoundComponent }
];@Input().Resolver : Précharge les données avant d'afficher la route.
provideRouter(routes, withComponentInputBinding());
@Component({})
export class CoinDetailComponent {
@Input() id!: string;
}
export const coinResolver = (route) => {
return inject(CryptoService).fetchCoin(route.params['id']);
};Formulaires
ArchitectureDeux approches : Template-driven et Reactive Forms.
Validators : Validation intégrée.
Typed Forms : Les formulaires sont typés.
import { FormGroup, FormControl, Validators, ReactiveFormsModule } from '@angular/forms';
@Component({
standalone: true,
imports: [ReactiveFormsModule],
template: `
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<input formControlName="email" placeholder="Email" />
<input formControlName="amount" type="number" />
<button [disabled]="form.invalid">Envoyer</button>
</form>
`
})
export class TransferComponent {
form = new FormGroup({
email: new FormControl('', [Validators.required, Validators.email]),
amount: new FormControl(0, [Validators.required, Validators.min(1)])
});
onSubmit() {
const { email, amount } = this.form.value;
}
}null ou un objet d'erreur.Validateur async : Retourne un Observable ou une Promise.
function strongPassword(control) {
const val = control.value;
const hasUpper = /[A-Z]/.test(val);
const hasNumber = /[0-9]/.test(val);
if (hasUpper && hasNumber) return null;
return { weakPassword: true };
}RxJS & Observables
AvancéRxJS gère les flux de données asynchrones dans Angular.
debounceTime : Attend X ms avant d'émettre.
combineLatest : Combine plusieurs flux.
results$ = this.searchControl.valueChanges.pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap(query => this.http.get(`/api/search?q=${query}`))
);toObservable() : Convertit un Signal en Observable.
import { toSignal, toObservable } from '@angular/core/rxjs-interop';Pipes
AvancéLes pipes transforment les données dans le template.
| currency : Affiche un montant en devise.
| async : Souscrit à un Observable automatiquement.
{{ today | date:'dd/MM/yyyy' }}
{{ 95000 | currency:'USD':'symbol':'1.0-0' }}pure : Recalcule seulement si l'input change.
@Pipe({ name: 'shortNumber', standalone: true })
export class ShortNumberPipe implements PipeTransform {
transform(value: number): string {
if (value >= 1000000) return (value / 1000000).toFixed(1) + 'M';
if (value >= 1000) return (value / 1000).toFixed(1) + 'K';
return value.toString();
}
}Lifecycle & Hooks
AvancéHooks du cycle de vie des composants et API modernes Angular.
ngOnChanges : Quand un input change.
ngOnDestroy : Nettoyage.
ngOnInit() {
this.loadChart(this.coinId);
}
ngOnDestroy() {
this.sub?.unsubscribe();
this.chart?.destroy();
}afterNextRender : Exécute du code après le prochain render.
import { DestroyRef, afterNextRender, inject } from '@angular/core';
@Component({})
export class ModernComponent {
private destroyRef = inject(DestroyRef);
constructor() {
afterNextRender(() => {
console.log('DOM prêt');
});
}
}Testing
AvancéAngular intègre un environnement de test avec TestBed.
ComponentFixture : Permet d'interagir avec le DOM du composant.
describe('CoinListComponent', () => {
it('should display coins', () => {
expect(true).toBe(true);
});
});