Patterns Angular
Comment créer un composant qui consomme Ori proprement.
Composer plutôt qu’étendre
Les composants Ori exposent des classes CSS sémantiques (ori-input,
ori-card, etc.) plus une API Angular standalone. La règle :
- Pour un cas standard : utiliser le composant tel quel
- Pour un variant qui revient souvent : créer un composant applicatif qui pré-configure
- Pour quelque chose de très différent : composer un nouveau composant à partir des classes CSS du DS
Exemple : wrapper d’un Input avec compteur de caractères
import { Component, Input } from '@angular/core';
import { OriInputComponent } from '@govpf/ori-angular';
@Component({
selector: 'app-input-with-counter',
standalone: true,
imports: [OriInputComponent],
template: `
<ori-input
[label]="label"
[(value)]="value"
[hint]="value.length + ' / ' + maxLength + ' caractères'"
[maxLength]="maxLength"
></ori-input>
`,
})
export class InputWithCounterComponent {
@Input() label = '';
@Input() maxLength = 280;
value = '';
}
Pas de fork du composant Input - juste un composant applicatif qui ajoute du comportement.
Réutiliser les classes CSS pour un composant 100% custom
Pour un composant qui n’existe pas dans Ori mais qui doit avoir le look du DS, réutiliser directement les classes CSS :
import { Component, Input } from '@angular/core';
@Component({
selector: 'app-result-card',
standalone: true,
template: `
<article class="ori-card ori-card--elevated" style="padding: 1.25rem;">
<h3 class="ori-card__title">{{ title }}</h3>
<p style="font-size: 2rem; font-weight: 600; margin: 0.5rem 0 0;">{{ count }}</p>
</article>
`,
})
export class ResultCardComponent {
@Input() title = '';
@Input() count = 0;
}
Le DS expose une trentaine de classes ori-* documentées dans
Fondations / Tokens / Sémantique.
Forms réactifs avec ReactiveFormsModule
Les composants form Ori n’implémentent pas ControlValueAccessor
nativement (volontaire pour rester découplés). Pour les utiliser avec
[formControl], wrapper :
import { Component, Input } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { OriInputComponent } from '@govpf/ori-angular';
@Component({
selector: 'app-rf-input',
standalone: true,
imports: [OriInputComponent, ReactiveFormsModule],
template: `
<ori-input
[label]="label"
[value]="control.value || ''"
(valueChange)="control.setValue($event)"
[error]="control.touched && control.invalid ? errorMessage : ''"
(blur)="control.markAsTouched()"
></ori-input>
`,
})
export class RfInputComponent {
@Input({ required: true }) control!: FormControl<string | null>;
@Input() label = '';
@Input() errorMessage = 'Champ invalide';
}
Pattern similaire pour <ori-checkbox>, <ori-select>, etc. À factoriser
en cas d’usages multiples.
Détection de changement OnPush
Tous les composants Ori utilisent ChangeDetectionStrategy.OnPush.
Cela signifie qu’ils ne se rafraîchissent que sur :
- Changement d’
@Input()(par référence pour les objets/tableaux) - Émission d’un
@Output() - Appel manuel à
markForCheck()/detectChanges()
Muter un objet en place (this.user.name = 'X') puis le passer à
un composant Ori ne déclenche pas le rendu. Solution : remplacer la
référence (this.user = { ...this.user, name: 'X' }).
Tableau avec rendu personnalisé par colonne
<ori-table> accepte un cellTemplate par colonne pour rendre une
cellule autrement que via {{ row[col.key] }}. Le piège : sous
strictTemplates, le contexte du template doit être typé exactement
{ $implicit: TRow; index: number }, et row côté template est
typé any, ce qui force à passer par des helpers typés.
import {
AfterViewInit,
Component,
TemplateRef,
ViewChild,
signal,
} from '@angular/core';
import {
OriTableComponent,
OriTagComponent,
type OriTableColumn,
} from '@govpf/ori-angular';
interface Dossier {
id: string;
numero: string;
statut: 'a_instruire' | 'en_cours' | 'valide' | 'rejete';
}
const VARIANT: Record<Dossier['statut'], 'info' | 'warning' | 'success' | 'danger'> = {
a_instruire: 'info',
en_cours: 'warning',
valide: 'success',
rejete: 'danger',
};
@Component({
selector: 'app-dossiers-table',
standalone: true,
imports: [OriTableComponent, OriTagComponent],
template: `
<ori-table
[columns]="columns()"
[rows]="rows"
[rowKey]="rowKey"
></ori-table>
<ng-template #statutTpl let-row>
<ori-tag [variant]="variant(row)">{{ row.statut }}</ori-tag>
</ng-template>
`,
})
export class DossiersTableComponent implements AfterViewInit {
rows: Dossier[] = [/* ... */];
rowKey = (r: Dossier) => r.id;
// Typage exact requis par OriTableColumn.cellTemplate.
@ViewChild('statutTpl', { static: true })
statutTpl!: TemplateRef<{ $implicit: Dossier; index: number }>;
columns = signal<OriTableColumn<Dossier>[]>([]);
// Helper typé : indispensable, parce que `row` côté template est `any`
// et indexer un Record<X, Y> avec `any` est refusé par strictTemplates.
variant(row: Dossier) {
return VARIANT[row.statut];
}
ngAfterViewInit() {
this.columns.set([
{ key: 'numero', label: 'Numéro', sortable: true },
{ key: 'statut', label: 'Statut', cellTemplate: this.statutTpl },
]);
}
}
Trois choses qui surprennent la première fois :
- Le
cellTemplateest défini côté composant via@ViewChild, pas côté template du parent. C’est le patternTemplateRefAngular standard. - Le typage
TemplateRef<{ $implicit: Dossier; index: number }>doit matcher exactement leOriTableColumn.cellTemplate; unTemplateRef<unknown>est refusé. let-rown’hérite pas du typage duTemplateRef; tout accès indexé surrowdoit passer par une méthode typée.
Pièges strict-templates récurrents
Inputs booléens
<!-- Refusé : passe la string "" au lieu de true -->
<ori-notification dismissible>
<!-- Bon -->
<ori-notification [dismissible]="true">
S’applique à tous les inputs booléens : dismissible, iconOnlyButton,
striped, loading, hideLabel, removable, etc.
Sélection multiple sur <ori-table>
selectable est une string union 'none' | 'single' | 'multiple',
pas un booléen. À passer comme attribut littéral ou string-bound :
<ori-table selectable="multiple" [rowKey]="rowKey" ...>
rowKey est obligatoire dès que selectable !== 'none'.
(select) du DropdownMenu : l’item peut être un séparateur
onMenuAction(item: OriDropdownMenuItem) {
// `separator: true` ne déclenche pas (select), mais `id` peut être
// undefined sur des items sans handler explicite. Toujours brancher
// sur item.id avec une garde.
switch (item.id) {
case 'voir': /* ... */ break;
case 'editer': /* ... */ break;
}
}
Anti-patterns
- ❌ Surcharger une classe
ori-*dans le CSS global pour la modifier. Ça casse la cohérence DS et le dark mode. - ❌ Utiliser
<button class="ori-button ori-button--primary">à la main au lieu de<ori-button variant="primary">. Tu perds le typage et l’a11y. - ❌ Importer plusieurs
BrowserModuleou autreModuleglobal - les composants Ori sont standalone et n’en demandent pas.