Au cours de la dernière année, Angular a vu de nombreuses nouvelles fonctionnalités apparaître, comme l'hydratation et les vues différables, pour aider les développeurs à améliorer leurs métriques Core Web Vitals et à offrir une expérience de qualité à leurs utilisateurs finaux. Des recherches sont également en cours sur d'autres fonctionnalités liées au rendu côté serveur qui s'appuient sur cette fonctionnalité, telles que le streaming et l'hydratation partielle.
Malheureusement, un modèle peut empêcher votre application ou votre bibliothèque de profiter pleinement de toutes ces nouvelles fonctionnalités à venir: la manipulation manuelle de la structure DOM sous-jacente. Angular exige que la structure du DOM reste cohérente à partir du moment où un composant est sérialisé par le serveur jusqu'à ce qu'il soit hydraté dans le navigateur. L'utilisation de ElementRef
, Renderer2
ou d'API DOM pour ajouter, déplacer ou supprimer manuellement des nœuds du DOM avant l'hydratation peut entraîner des incohérences qui empêchent ces fonctionnalités de fonctionner.
Cependant, toutes les manipulations et les accès manuels du DOM ne sont pas problématiques, et parfois, ils sont nécessaires. Pour utiliser le DOM de manière sécurisée, vous devez le solliciter le moins possible, puis le retarder le plus longtemps possible. Les consignes suivantes vous expliquent comment y parvenir et créer des composants Angular véritablement universels et évolutifs qui peuvent tirer pleinement parti de toutes les nouvelles fonctionnalités d'Angular.
Éviter la manipulation manuelle du DOM
Le meilleur moyen d'éviter les problèmes causés par la manipulation manuelle du DOM est, sans surprise, de ne pas la faire du tout dans la mesure du possible. Angular dispose d'API et de modèles intégrés qui peuvent manipuler la plupart des aspects du DOM. Nous vous recommandons de les utiliser plutôt que d'accéder directement au DOM.
Muter l'élément DOM d'un composant
Lorsque vous écrivez un composant ou une directive, vous devrez peut-être modifier l'élément hôte (c'est-à-dire l'élément DOM correspondant au sélecteur du composant ou de la directive) pour ajouter, par exemple, une classe, un style ou un attribut, plutôt que de cibler ou d'introduire un élément wrapper. Il est tentant de simplement utiliser ElementRef
pour modifier l'élément DOM sous-jacent. Utilisez plutôt des liaisons d'hôte pour lire de manière déclarative les valeurs à une expression:
@Component({
selector: 'my-component',
template: `...`,
host: {
'[class.foo]': 'true'
},
})
export class MyComponent {
/* ... */
}
Tout comme pour la liaison de données en HTML, vous pouvez également, par exemple, lier des attributs et des styles, et remplacer 'true'
par une autre expression qu'Angular utilisera pour ajouter ou supprimer automatiquement la valeur si nécessaire.
Dans certains cas, la clé doit être calculée de manière dynamique. Vous pouvez également vous lier à un signal ou à une fonction qui renvoie un ensemble ou une mappe de valeurs:
@Component({
selector: 'my-component',
template: `...`,
host: {
'[class.foo]': 'true',
'[class]': 'classes()'
},
})
export class MyComponent {
size = signal('large');
classes = computed(() => {
return [`size-${this.size()}`];
});
}
Dans les applications plus complexes, il peut être tentant de recourir à la manipulation manuelle du DOM pour éviter une ExpressionChangedAfterItHasBeenCheckedError
. Vous pouvez plutôt lier la valeur à un signal, comme dans l'exemple précédent. Vous pouvez le faire si nécessaire, et cela ne nécessite pas d'adopter des signaux dans l'ensemble de votre codebase.
Muter des éléments DOM en dehors d'un modèle
Il est tentant d'essayer d'utiliser le DOM pour accéder à des éléments normalement inaccessibles, tels que ceux qui appartiennent à d'autres composants parents ou enfants. Toutefois, cette approche est sujette aux erreurs, ne respecte pas l'encapsulation et rend difficile la modification ou la mise à niveau de ces composants à l'avenir.
À la place, votre composant doit considérer tous les autres composants comme une boîte noire. Prenez le temps de réfléchir à quand et où d'autres composants (même au sein de la même application ou bibliothèque) peuvent avoir besoin d'interagir avec le comportement ou l'apparence de votre composant, ou de le personnaliser, puis exposez un moyen sûr et documenté de le faire. Utilisez des fonctionnalités telles que l'injection de dépendance hiérarchique pour rendre une API disponible pour un sous-arbre lorsque les propriétés @Input
et @Output
simples ne suffisent pas.
Historiquement, il était courant d'implémenter des fonctionnalités telles que des boîtes de dialogue modales ou des info-bulles en ajoutant un élément à la fin de <body>
ou d'un autre élément hôte, puis en y déplaçant ou en y projetant du contenu. Cependant, vous pouvez désormais afficher un élément <dialog>
simple dans votre modèle:
@Component({
selector: 'my-component',
template: `<dialog #dialog>Hello World</dialog>`,
})
export class MyComponent {
@ViewChild('dialog') dialogRef!: ElementRef;
constructor() {
afterNextRender(() => {
this.dialogRef.nativeElement.showModal();
});
}
}
Différer la manipulation manuelle du DOM
Après avoir suivi les consignes précédentes pour minimiser la manipulation directe du DOM et accéder autant que possible, il est possible que vous ne puissiez pas éviter certaines manipulations. Dans ce cas, il est important de le différer le plus longtemps possible. Les rappels afterRender
et afterNextRender
sont un excellent moyen d'y parvenir, car ils ne s'exécutent que dans le navigateur, une fois qu'Angular a détecté les modifications et les a validées dans le DOM.
Exécuter du code JavaScript uniquement dans le navigateur
Dans certains cas, vous disposez d'une bibliothèque ou d'une API qui ne fonctionne que dans le navigateur (par exemple, une bibliothèque de graphiques, une utilisation de IntersectionObserver
, etc.). Au lieu de vérifier de manière conditionnelle si vous exécutez le code dans le navigateur ou de simuler le comportement sur le serveur, vous pouvez simplement utiliser afterNextRender
:
@Component({
/* ... */
})
export class MyComponent {
@ViewChild('chart') chartRef: ElementRef;
myChart: MyChart|null = null;
constructor() {
afterNextRender(() => {
this.myChart = new MyChart(this.chartRef.nativeElement);
});
}
}
Effectuer une mise en page personnalisée
Il peut arriver que vous deviez lire ou écrire dans le DOM pour effectuer une mise en page que vos navigateurs cibles ne prennent pas encore en charge, par exemple pour positionner une info-bulle. afterRender
est un excellent choix pour cela, car vous pouvez être sûr que le DOM est dans un état cohérent. afterRender
et afterNextRender
acceptent une valeur phase
de EarlyRead
, Read
ou Write
. Lire la mise en page DOM après l'avoir écrite oblige le navigateur à recalculer la mise en page de manière synchrone, ce qui peut sérieusement affecter les performances (voir dégradation de la mise en page). Il est donc important de diviser soigneusement votre logique en phases appropriées.
Par exemple, un composant d'info-bulle qui souhaite afficher une info-bulle par rapport à un autre élément de la page utilise probablement deux phases. La phase EarlyRead
est d'abord utilisée pour acquérir la taille et la position des éléments:
afterRender(() => {
targetRect = targetEl.getBoundingClientRect();
tooltipRect = tooltipEl.getBoundingClientRect();
}, { phase: AfterRenderPhase.EarlyRead },
);
Ensuite, la phase Write
utilisera la valeur précédemment lue pour repositionner l'info-bulle:
afterRender(() => {
tooltipEl.style.setProperty('left', `${targetRect.left + targetRect.width / 2 - tooltipRect.width / 2}px`);
tooltipEl.style.setProperty('top', `${targetRect.bottom - 4}px`);
}, { phase: AfterRenderPhase.Write },
);
En divisant notre logique en phases appropriées, Angular peut effectuer efficacement la manipulation du DOM par lot sur tous les autres composants de l'application, ce qui garantit un impact minimal sur les performances.
Conclusion
De nombreuses améliorations intéressantes sont à venir pour le rendu côté serveur Angular, afin de vous permettre de proposer plus facilement une expérience de qualité à vos utilisateurs. Nous espérons que les conseils précédents vous aideront à exploiter pleinement ces fonctionnalités dans vos applications et bibliothèques.