📧 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://www.easyangularkit.com/easyangularkit

📧 Reste informé(e) !

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

S'inscrire gratuitement

AngularKit

Suite d'outils pour développeurs Angular francophones. Apprends, modernise tes réflexes, audite ta codebase.

Produits

Contact

Légal

© 2026 AngularKit. Tous droits réservés.