📧 Reste informé(e) !

Reçois les derniers articles et conseils EasyAngularKit directement dans ta boîte mail.

S'inscrire gratuitement

~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 ?

  1. Confusion : tout est un input, même les configs statiques
  2. Verbosité : trop d'attributs sur les composants
  3. Performance : détection de changements inutile
  4. 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

📧 Reste informé(e) !

Reçois les derniers articles et conseils EasyAngularKit directement dans ta boîte mail.

S'inscrire gratuitement

EasyAngularKit

Formation complète pour maîtriser Angular et développer des applications web modernes.

Navigation

Contact

Légal

© 2026 Easy Angular Kit. Tous droits réservés.