~3 min de lecture
input(), @Attribute(), ou InjectionToken ? Le Guide Pour Choisir La Bonne Approche
Tu hésites entre input(), @Attribute() et InjectionToken pour passer des données à ton composant ? Tu ne sais pas
lequel utiliser ?
Dans cet article, je vais te montrer les cas d'usage de chacun et quand utiliser l'un plutôt que les autres.
Le Problème : 3 Façons De Passer Des Données
❌ Ce que font les juniors :
Ils utilisent input() pour TOUT, même quand ce n'est pas approprié.
@Component({
selector: 'app-button',
template: `<button [type]="buttonType()">{{ label() }}</button>`
})
export class ButtonComponent {
// ❌ Utilisé pour des valeurs statiques
readonly buttonType = input<string>('button');
readonly label = input<string>('Click');
readonly color = input<string>('primary');
}
// ❌ Utilisé partout dans le template
<app-button buttonType="submit" label="Envoyer" color="primary"/>
<app-button buttonType="button" label="Annuler" color="secondary"/>
🚨 Pourquoi c'est problématique ?
- Confusion : tout est un input, même les configs statiques
- Verbosité : trop d'attributs sur les composants
- Performance : détection de changements inutile
- Pas scalable : difficile de partager des configs
Les 3 Approches Expliquées
1️⃣ input() : Pour Les Données Réactives
Utilise input() quand :
✅ Les données peuvent changer après l'initialisation
✅ Le parent contrôle la valeur
✅ Tu as besoin de réactivité (computed, effect)
@Component({
selector: 'app-counter',
template: `
<p>Count: {{ count() }}</p>
<p>Double: {{ double() }}</p>
`
})
export class CounterComponent {
// ✅ input() : valeur réactive du parent
readonly count = input.required<number>();
// ✅ Computed basé sur l'input
readonly double = computed(() => this.count() * 2);
}
// Utilisation : la valeur peut changer
<app-counter [count]="currentCount()"/>
2️⃣ @Attribute() : Pour Les Valeurs Statiques
Utilise @Attribute() quand :
✅ La valeur ne change JAMAIS après l'init
✅ C'est une configuration statique
✅ Tu veux optimiser les performances
@Component({
selector: 'app-icon',
template: `
<svg [attr.width]="size" [attr.height]="size">
<use [attr.href]="'#icon-' + name" />
</svg>
`
})
export class IconComponent {
// ✅ @Attribute() : valeur statique
readonly name: string;
readonly size: string;
constructor(
@Attribute('name') name: string,
@Attribute('size') size: string = '24'
) {
this.name = name;
this.size = size;
}
}
// Utilisation : valeurs fixes
<app-icon name="user" size="32"/>
Depuis la v17.3.0 On peut l'écrire comme ça :
@Component({
selector: 'app-icon',
template: `
<svg [attr.width]="size" [attr.height]="size">
<use [attr.href]="'#icon-' + name" />
</svg>
`
})
export class IconComponent {
readonly name: string = inject(new HostAttributeToken('name'));
readonly size: string = inject(new HostAttributeToken('size'), { optional: true }) ?? 24;
}
3️⃣ InjectionToken : Pour La Configuration Globale
Utilise InjectionToken quand :
✅ Configuration partagée entre plusieurs composants
✅ Valeurs globales de l'application
✅ Tu veux des valeurs par défaut personnalisables
// Configuration globale
export type ButtonConfig = {
readonly defaultVariant: 'primary' | 'secondary';
readonly defaultSize: 'sm' | 'md' | 'lg';
};
export const BUTTON_CONFIG = new InjectionToken<ButtonConfig>('BUTTON_CONFIG', {
providedIn: 'root',
factory: () => ({
defaultVariant: 'primary',
defaultSize: 'md'
})
});
@Component({
selector: 'app-button',
template: `<button [class]="buttonClass()"><ng-content /></button>`
})
export class ButtonComponent {
private readonly config = inject(BUTTON_CONFIG);
// ✅ input() pour override la config
readonly variant = input<'primary' | 'secondary'>(this.config.defaultVariant);
readonly size = input<'sm' | 'md' | 'lg'>(this.config.defaultSize);
readonly buttonClass = computed(() =>
`btn btn-${this.variant()} btn-${this.size()}`
);
}
// Config globale
export const appConfig: ApplicationConfig = {
providers: [
{
provide: BUTTON_CONFIG,
useValue: {
defaultVariant: 'secondary',
defaultSize: 'lg'
}
}
]
};
Comparaison Détaillée
| Critère | input() | @Attribute() | InjectionToken |
|---|---|---|---|
| Réactivité | ✅ Oui | ❌ Non | ❌ Non |
| Change Detection | ✅ Détecté | ❌ Ignoré | ❌ Ignoré |
| Performance | 🟡 Moyenne | ✅ Excellente | ✅ Excellente |
| Cas d'usage | Données dynamiques | Valeurs statiques | Config globale |
| Scope | Composant | Composant | Application |
| Override | ✅ Facile | ❌ Impossible | 🟡 Par provider |
Cas d'Usage Réels
Exemple 1 : Composant Button
// button-config.token.ts
export type ButtonConfig = {
readonly defaultVariant: 'primary' | 'secondary' | 'danger';
readonly defaultSize: 'sm' | 'md' | 'lg';
};
export const BUTTON_CONFIG = new InjectionToken<ButtonConfig>('BUTTON_CONFIG');
// button.ts
@Component({
selector: 'app-button',
template: `
<button
[type]="type"
[class]="buttonClass()"
[disabled]="disabled()">
<ng-content />
</button>
`
})
export class Button {
private readonly config = inject(BUTTON_CONFIG, { optional: true });
// ❌ @Attribute() : type ne change jamais
readonly type: string = inject(new HostAttributeToken('size'), { optional: true }) ?? 'button';
// ✅ input() : valeurs qui peuvent changer
readonly variant = input<'primary' | 'secondary' | 'danger'>(
this.config?.defaultVariant ?? 'primary'
);
readonly size = input<'sm' | 'md' | 'lg'>(this.config?.defaultSize ?? 'md');
readonly disabled = input<boolean>(false);
readonly buttonClass = computed(() =>
`btn btn-${this.variant()} btn-${this.size()}`
);
}
<!-- Utilisation -->
<app-button type="submit" [variant]="'primary'" [size]="'lg'">Envoyer</app-button>
// app.config.ts
export const appConfig: ApplicationConfig = {
providers: [
{
provide: BUTTON_CONFIG,
useValue: {
defaultVariant: 'secondary',
defaultSize: 'lg',
},
},
],
};
Exemple 2 : Composant Icon
@Component({
selector: 'app-icon',
template: `
<svg
[attr.width]="size()"
[attr.height]="size()"
[attr.fill]="color()">
<use [attr.href]="'#icon-' + name" />
</svg>
`
})
export class Icon {
// ❌ @Attribute() : nom statique qui ne change pas
readonly name: string = inject(new HostAttributeToken('name'));
// ✅ input() : taille peut être dynamique
readonly size = input<number>(24);
readonly color = input<string>('currentColor');
}
// Utilisation
<app-icon name="user" [size
]
= "iconSize()" [color] = "iconColor()" / >
Quand Utiliser Quoi ?
Utilise input() pour :
✅ Données du parent qui peuvent changer
✅ Props réactifs (avec computed, effect)
✅ Valeurs bindées [prop]="value"
✅ Communication parent-enfant
class Component {
readonly count = input.required<number>();
readonly user = input<User | null>(null);
readonly items = input<Item[]>([]);
}
Utilise @Attribute() pour :
✅ Configuration statique (id, name, type)
✅ Valeurs HTML natives (role, aria-*)
✅ Optimisation (pas de change detection)
✅ Métadonnées qui ne changent jamais
class Component {
constructor(
@Attribute('id') readonly id: string,
@Attribute('type') readonly type: string = 'text',
@Attribute('role') readonly role: string = 'button',
) {
}
}
ou
class Component {
readonly id: string = inject(new HostAttributeToken('id'));
readonly type: string = inject(new HostAttributeToken('type'), { optional: true }) ?? 'text';
readonly role: string = inject(new HostAttributeToken('role'), { optional: true }) ?? 'button';
}
Utilise InjectionToken pour :
✅ Configuration globale de l'app
✅ Valeurs partagées entre composants
✅ Thèmes et styles globaux
✅ Feature flags et options
export const APP_CONFIG = new InjectionToken<AppConfig>('APP_CONFIG');
export const THEME_CONFIG = new InjectionToken<ThemeConfig>('THEME_CONFIG');
export const FEATURE_FLAGS = new InjectionToken<FeatureFlags>('FEATURE_FLAGS');
Les Pièges À Éviter
Piège 1 : Utiliser input() pour des valeurs statiques
class Component {
// ❌ Mauvais : type ne change jamais
readonly type = input<string>('button');
// ✅ Bon : @Attribute() pour valeur statique
constructor(@Attribute('type') readonly type: string = 'button') {
}
}
Piège 2 : Utiliser @Attribute() pour des valeurs dynamiques
class Component {
// ❌ Mauvais : disabled peut changer
constructor(@Attribute('disabled') readonly disabled: string) {
}
// ✅ Bon : input() pour valeur réactive
readonly disabled = input<boolean>(false);
}
Piège 3 : Dupliquer la config dans chaque composant
// ❌ Mauvais : config répétée partout
@Component({ /* ... */ })
export class ButtonComponent {
readonly defaultVariant = 'primary';
readonly defaultSize = 'md';
}
@Component({ /* ... */ })
export class InputComponent {
readonly defaultSize = 'md'; // Dupliqué
}
// ✅ Bon : InjectionToken partagé
export const UI_CONFIG = new InjectionToken<UiConfig>('UI_CONFIG');
Piège 4 : Oublier optional sur InjectionToken
class Component {
// ❌ Erreur si token non fourni
private readonly config = inject(BUTTON_CONFIG);
// ✅ Bon : optional avec valeur par défaut
private readonly config = inject(BUTTON_CONFIG, { optional: true });
readonly variant = input(this.config?.defaultVariant ?? 'primary');
}
Pattern Hybride (Recommandé)
Combine les 3 approches pour un composant flexible :
@Component({
selector: 'app-form-field',
template: `
<div class="field" [class]="fieldClass()">
<label [for]="id">{{ label() }}</label>
<input
[id]="id"
[type]="type"
[value]="value()"
[disabled]="disabled()"
/>
</div>
`
})
export class FormField {
// 🔵 InjectionToken : config globale
private readonly formConfig = inject(FORM_CONFIG, { optional: true });
// 🟡 @Attribute() : valeurs statiques
readonly id: string;
readonly type: string;
// 🟢 input() : valeurs réactives
readonly label = input.required<string>();
readonly value = input<string>('');
readonly disabled = input<boolean>(false);
readonly size = input<'sm' | 'md' | 'lg'>(
this.formConfig?.defaultSize ?? 'md'
);
readonly fieldClass = computed(() => `field field-${this.size()}`);
constructor(
@Attribute('id') id: string,
@Attribute('type') type: string = 'text'
) {
this.id = id;
this.type = type;
}
}
Conclusion : Choisis La Bonne Approche
Chaque approche a son cas d'usage spécifique.
Ce que tu dois retenir :
✅ input() = données réactives du parent
✅ @Attribute() = valeurs statiques (perf)
✅ InjectionToken = configuration globale
✅ Combiner les 3 pour des composants flexibles
✅ Performance : préfère @Attribute() quand possible
La règle d'or :
Si ça peut changer → input(). Si c'est statique → @Attribute(). Si c'est global → InjectionToken.
Envie de découvrir EasyAngularKit et son programme ?
→ Découvre EasyAngularKit : https://pim.ms/C31g7p2