Dans le dernier article nous avions notre composant qui s’occupait de s’extraire et une directive qui s’occupait de le remettre dans une boite, en gros. Mais bon, si le composant pouvait éviter de s’extraire pour être remis en boite ça serait quand même mieux.
Du coup l’idée est de lui rendre la notion « es-tu une ligne ? » via un @Input() row: boolean = true; En considérant qu’il l’est par défaut (au choix). Ensuite quand on crée le composant on agit selon la valeur de row.
À l’initialisation, si nous somme une ligne, nous allons transformer notre template enfant #tref en contenu de notre propre bloc via le ViewContainerRef pour le créer et le Renderer2 qui va l’injecter dans le nœud de notre composant courant. Ensuite on peut signaler à notre bloc courant, le composant en lui-même, que nous aimerions le décorer de quelques classes CSS pour en faire une row css au sens Bootstrap.
if (this.row) {
// Transform template into inside elements
Promise.resolve(null).then(() => {
const view = this.vcRef.createEmbeddedView(this.tpl);
view.rootNodes.formEach((node) => this.renderer.appendChild(this.el.nativeElement, node));
});
// Transform parent to row with classes
this.renderer.setAttribute(this.el.nativeElement, 'class', "row " + this.classes);
}
Quand l’élément est prêt, et que nous ne sommes pas une ligne, on continue de sortir notre contenu en dehors du bloc par la même mécanique. On ajoute une classe css au bloc courant pour le masquer (bootstrap: d-none -> display: none) et par la même mécanique que le paragraphe précédent on utilise le template et l’ajoute un cran plus haut du coup. Aussi simple que ça.
Dans cet article nous parlerons d’Angular 11, de bootstrap 5, de component, de directive, de sortir le contenu d’un component au niveau du parent et d’encapsuler un contenu, le tout autour du sujet de grille bootstrap (row/col) et d’imbrications posant problème au sujet des alignements.
Vous avez surement déjà utilisé le système de grille de bootstrap et vous connaissez le principe d’un bloc dans un autre tel des poupées Russes et des Legos. Définissons alors un composant vous permettant d’afficher une zone de texte (input) précédé de son nom/étiquette (label).
En Angular nous créerons un component pour réutiliser ce système, nous n’aurons qu’à lui passer le libellé du champs, son formGroup si applicable, etc.
Dans notre exemple nous donnons une répartition 1/3 , 2/3.
Ça c’est un cas simple mais imaginons maintenant un cas où nous aurions 2 colonnes contenant chacune le composant mon-input, suivi d’une ligne avec un composant mon-input mais qui voudrait faire la largeur des 2 colonnes.
Voici ce que donnera la découpe logique une fois mis en bootstrap.
La première ligne donne le bon résultat : 2 colonnes chacune occupant 1/3 , 2/3 de l’espace parent. Premier problème : utiliser 1/3 , 2/3 sur la seconde ligne ne fonctionne pas.
Première idée du coup changer la répartition, et on peut essayer… ça n’ira pas. Bootstrap nous donne un conteneur row équivalent à 100% de l’espace divisé en 12 colonne théorique.
Dans notre exemple nous avons donc une coupe en 50/50 (soit 2 * 6), sans oublier qu’il y a une gouttière entre les cellule. Nous voudrions donc que la seconde ligne soit dans une répartition 1/12 ou 2/12 pour le label et le reste à l’input.
L’idée de cet article n’est pas le CSS, la mise en page en trichant ou en cassant les mécaniques rencontrées mais de faire avec naturellement et de solutionner par la construction.
Pour ne pas faire durer inutilement, la solution est que la première ligne ne doit pas être en 2 colonnes mais en une seule et de donner des 12ème de la largeur entière au 2 premiers composants. Le hic, car il y a toujours un hic, c’est qu’Angular encapsule ses composants. Au niveau du rendu en 2 colonnes vous auriez quelque chose comme :
Nous y voilà, c’est plus clair : le soucis sont les tags <mon-input>, ceux-ci considéré par Bootstrap comme des cellules sans la classe CSS adéquate (col-x). Et non, comme par magie il ne considérera pas les enfants de mon-input. De plus la répartition cumulée des col-4 et col-8 dans cette ligne dépasse 12.
Voici ce que nous voudrions, tout en ayant un composant (!) :
C’est là que nous parlons de décapsuler (unwrap) notre composant pour que son host (bloc parent auto créé) ne viennent pas entraver notre volonté. La technique est simple et pourtant pas évidente à trouver, et au travers du temps, beaucoup de chose ne fonctionnent plus (ancienne version d’Angular) ou pas encore (CSS working draft ou roadmap Angular).
La solution théorique est de considérer l’intérieur de son composant comme un template
et de le cibler et le mettre en dehors avec la commande :
this.vcRef.createEmbeddedView(this.tpl);
Selon votre implémentation vous aurez peut-être comme résultat de dupliquer votre composant, une fois à l’intérieur et une fois à l’extérieur, du coup je suis parti sur une transformation de l’HTML du composant via un <ng-template #tref> et cibler celui-ci dans ma mécanique avec un :
@ViewChild('tref', { static: true }) tpl;
Ainsi dans votre ngAfterViewInit (ou ngOnInit) vous pourrez utiliser la ligne createEmbeddedView du vcRef (ViewContainerRef).
À noter qu’il faut ajouter dans @Component l’attribut host:style pour qu’il n’affecte plus le dom, car il reste présent malgré tout.
Vous me direz que l’on perd la notion de gouttière mais en fait non, on reste en contextes de colonnes, mais vous perdrez le cumul inutile, pour autant qu’il se marque.
Maintenant parlons de cette seconde ligne, toujours en utilisant notre composant mon-input. Vous me direz suffit de lui mettre un <div class= »row »> autour et le c’est fait. C’est pas tout à fait faux, en considérant que l’on lui envoie la nouvelle répartition des colonnes par @Input(), ce que j’ai fait chez moi, pour démo :
Ceci étant dit, nous ne voulons pas mettre à la main le <div class= »row »>, nous voulons que cela se fasse tout seul. Vous allez rire mais nous allons réencapsuler ce que nous venons de décapsuler. À ce stade je n’ai pas trouvé mieux pour le moment.
Pour ce faire c’est plus compliqué, étant un composant nous ne pouvons pas utiliser un sélecteur [mon-selecteur] d’un composant (component), qui se prendrait pour une directive, pour jouer sur le <ng-content> d’un template : <div class= »row »><ng-content></ng-content></div>.
Nous allons devoir utiliser une directive et ce qui est amusant c’est que la directive va appeler un composant pour « composer » notre sortie.
L’idée est que la directive considère le tag (mon-input dans notre cas) sur laquelle elle est comme le template à injecter dans le composant d’encapsulation. Ainsi, nous créons l’encapsulation, on récupère le template, on l’injecte, on peut même le paramétrer et le tour est joué. On passerait de :
C’est terrible ! Exactement ce que l’on souhaite avoir.
Côté technique vous aurez la directive [bs-row] initiant l’encapsulation, et le component ‘bs-row-container’ ne contenant qu’un template rudimentaire :