~5 min de lecture
Et si withComponentInputBinding était plus structurant qu’on ne le croit ?
withComponentInputBinding() est un petit helper arrivé avec Angular v16 (puis amélioré en v17+), mais qui peut avoir un impact sur certains de tes choix.
🧩 Le problème avant
Avant, pour récupérer les paramètres de route dans un composant, même en full Signals, il fallait passer par
ActivatedRoute et toSignal :
@Component({
standalone: true,
template: `Profil de {{ userId() }}`
})
export default class ProfilPage {
private route = inject(ActivatedRoute);
readonly userId = toSignal(
this.route.paramMap.pipe(map(params => params.get('userId') ?? 'anonyme'))
);
}
Même avec Signals, on reste dépendant de RxJS + boilerplate + complexité.
✅ Ce qu’apporte withComponentInputBinding
Pour activer cette fonctionnalité, il faut ajouter withComponentInputBinding() dans la configuration du routeur
global :
bootstrapApplication(App, {
providers: [
provideRouter(routes, withComponentInputBinding())
]
});
Et dans le fichier de routes :
import { Routes } from '@angular/router';
export const routes: Routes = [
{ path: 'profil/:userId', loadComponent: () => import('./profil.page') }
];
Puis, dans le composant :
@Component({
standalone: true,
template: `Profil de {{ userId() }}`
})
export default class ProfilPage {
readonly userId = input<string>();
}
➡️ Le paramètre :userId est automatiquement injecté dans l’input userId.
➡️ Plus besoin d’injecter ActivatedRoute, ni de transformer quoi que ce soit.
➡️ Valable avec les routes params, queryParams, data et resolver.
➡️ Tes composants deviennent donc plus facilement réutilisables, puisqu'ils ne sont plus directement liés à l'ActivatedRoute.
🧠 Comparaison AVANT / APRÈS
| Aspect | Avant | Après |
|---|---|---|
| Lecture des params | ActivatedRoute + toSignal() |
input() |
| Boilerplate | élevé | minimal |
| Dépendances | RxJS, ActivatedRoute | Angular Signals uniquement |
| Tests | plus difficiles | plus simples (inputs injectables) |
| Clarté de lecture | moyenne | excellente |
⚠️ Un comportement à connaître : valeurs par défaut ignorées
Un piège assez subtil existe :
Si la route n’injecte aucune valeur dans un input défini avec input('valeurParDefaut'), la valeur par défaut est ignorée.
Exemple :
@Component({
standalone: true,
template: `Profil de {{ userId() }}`
})
export default class ProfilPage {
readonly userId = input('anonyme');
}
Mais si la route ne définit pas :userId, le composant reçoit... undefined.
Pourquoi ? Parce qu’Angular assigne explicitement
undefinedà tout input lié, mais non présent dans la route, ce qui écrase la valeur par défaut.
Ce comportement a été discuté dans une issue GitHub où j’ai proposé une amélioration pour conserver les valeurs par défaut quand aucun paramètre n’est fourni.
Cela a été refusé à l'époque, mais Angular 22 a fini par livrer une option qui traite le cas (voir plus bas).
Et input.required() ne te protège pas non plus
Le réflexe naturel face à ce comportement, c'est de rendre l'input obligatoire : si la route ne fournit rien, input.required() devrait crier. Il ne crie pas.
@Component({
template: `Profil de {{ userId() }}`
})
export default class ProfilPage {
readonly userId = input.required<string>();
}
Avec la route { path: 'profil', component: ProfilPage } (pas de :userId), le template rend Profil de , userId() vaut undefined, et aucune NG0950 n'est levée - y compris en mode dev, là où on l'attendrait.
La raison est mécanique. input.required() démarre sur une sentinelle : une valeur interne réservée qui ne signifie pas undefined, mais « personne n'a encore écrit ici ». NG0950 n'est levée que tant que cette sentinelle est en place. Or ici le Router écrit : il écrit undefined, ce qui la remplace. Une valeur écrite, fût-elle undefined, sort définitivement l'input de l'état « pas encore fourni » - rien ne restaure jamais la sentinelle. Le garde-fou est désamorcé par le mécanisme même qui cause le bug.
✅ Angular 22 : l'option unmatchedInputBehavior
Mise à jour (Angular 22) :
withComponentInputBinding()accepte désormais un objet d'options, dontunmatchedInputBehavior, qui traite le problème à la racine. L'option n'existe pas en Angular 21 et en dessous. Le tour d'horizon de la release est dans le guide Angular 22.
bootstrapApplication(App, {
providers: [
provideRouter(
routes,
withComponentInputBinding({ unmatchedInputBehavior: 'undefinedIfStale' })
)
]
});
Deux valeurs possibles :
'alwaysUndefined'(défaut, comportement historique) : le Router écritundefineddans tout input non fourni.'undefinedIfStale': le Router n'écritundefinedque si la clé a déjà été fournie pendant la vie de la route active dans cet outlet - c'est le cas stale, la donnée devenue obsolète, qu'il faut bien nettoyer. Un input que la route n'a jamais renseigné n'est, lui, jamais écrit du tout.
Ce que ça change, mesuré sur Angular 22 :
| Situation | alwaysUndefined (défaut) |
undefinedIfStale |
|---|---|---|
input('anonyme') jamais fourni |
undefined |
'anonyme' |
input('anonyme') fourni à la navigation précédente, absent de celle-ci |
undefined |
undefined |
input.required() jamais fourni |
undefined, sans erreur |
NG0950 |
Lis bien la colonne Situation : les deux garanties valent pour une clé que la route ne fournit jamais - précisément le trou que cet article décrit. Là, la valeur par défaut survit et required retrouve son NG0950.
Dès qu'une clé a été fournie une fois pendant l'activation courante de l'outlet, en revanche, undefinedIfStale se comporte comme alwaysUndefined : elle réécrit undefined. C'est le but - la donnée obsolète doit partir - mais sur un input required, elle l'écrase là aussi en silence, sans NG0950.
Encore faut-il que l'outlet garde la même route activée - passer par une autre route repart de zéro et la valeur par défaut revient. Autrement dit : les query params qui vont et viennent sur une même page - filtres, recherche, tri, pagination. C'est le cas d'usage canonique de withComponentInputBinding(), pas un cas marginal.
🛠 Contournements avant Angular 22
Si tu es encore en Angular 21 ou en dessous, voici les trois options.
Valeur par défaut dans la route
Pour éviter ce comportement, il faut toujours fournir une valeur dans la configuration de la route, par exemple :
export const routes: Routes = [
{
path: 'profil',
// injecté dans l'input
data: { userId: 'anonyme' },
loadComponent: () => import('./profil.page'),
}]
computed / linkedSignal
Ou bien gérer l’input côté composant :
export class ProfilPage {
readonly userId = input<string | undefined>();
readonly safeUserId = computed(() => this.userId() ?? 'anonyme');
}
Input transform
Ou encore utiliser la fonction de transformation de ton input :
export class ProfilPage {
readonly userId = input<string | undefined, string>(undefined, {
transform: (value) => (value === undefined ? '2' : value),
});
}
🧱 Conclusion
withComponentInputBinding()rend tes composants plus concis.- Il s’intègre parfaitement avec les Signals.
- Et il faut être attentif aux valeurs non fournies, sous peine d’écraser les valeurs par défaut.
- Ne compte pas sur
input.required()pour te protéger : sous le comportement par défaut il ne lève rien et te rendundefined, exactement l'inverse de l'intuition. - En Angular 22, pose
unmatchedInputBehavior: 'undefinedIfStale': les valeurs par défaut survivent, etrequiredretrouve sonNG0950- pour les clés que ta route ne fournit jamais, pas pour celles qui disparaissent en cours de route. - En Angular 21 et en dessous, pars du principe que la valeur peut être
undefinedet type tes inputs en conséquence.
Tu veux aller plus loin sur le Routing Angular, en pratique ?
➡️ Découvre le Module 5 d’EasyAngularKit - Maîtriser le routing (v19)