Projection de contenu dans une documentation angulaire ou à contenu ng perdu

En apprenant Angular, très souvent ils manquent ou ne prêtent pas assez d'attention à un concept tel que la projection de contenu. Il s'agit d'un outil très puissant pour créer des composants flexibles et réutilisables. Mais sa documentation ne mentionne que quelques paragraphes dans la section Crochets du cycle de vie . Essayons de corriger cette omission.



Projection de contenu à l'aide de ng-content


La projection de contenu est un moyen d'importer du contenu HTML de l'extérieur d'un composant et de le coller dans un modèle de composant à un emplacement spécifique. (traduction gratuite de la documentation)
La définition est assez compliquée, mais en fait, tout est beaucoup plus simple. Nous avons une sorte de composant, et tout ce qui se trouve entre ses balises d'ouverture et de fermeture est du contenu.

<app-parent>
    <!-- content -->
    I'm content!
    <!-- content -->
</app-parent>

Et Angular vous permet d'incorporer n'importe quel code HTML (contenu) dans le modèle de ce composant à l'aide d'un élément ng-content.

Essayons de comprendre pourquoi cela est nécessaire et comment cela fonctionne avec un exemple. Disons que nous avons un simple bouton. Le texte de ce bouton est transmis au modèle input property.

// button.component.ts
import {Component, Input} from '@angular/core';

@Component({
  selector: 'app-button',
  template: '<button>{{text}}</button>'
})
export class ButtonComponent {
  @Input() text: string;
}

// app.component.ts
import {Component} from '@angular/core';

@Component({
  selector: 'app-root',
  template: `<app-button [text]="'Button'"></app-button>`,
})
export class AppComponent {
}

Cela semble bien. Mais soudain, nous avons dû ajouter une icône au texte pour certains boutons. Nous avons déjà un composant icône. Il vous suffit de l'ajouter au modèle de bouton, de joindre une directive ngIfet d'en écrire une autre input propertypour l'affichage dynamique de l'icône.

// icon.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-icon',
  template: '☻',
})
export class IconComponent {
}

// button.component.ts
import {Component, Input} from '@angular/core';

@Component({
  selector: 'app-button',
  template: `<button>
               <app-icon *ngIf="showIcon"></app-icon>
               {{text}}
             </button>`,
})
export class ButtonComponent {
  @Input() text: string;
  @Input() showIcon = true;
}

Tout fonctionne. Mais que se passe-t-il si vous devez modifier l'emplacement de l'icône par rapport au texte? Ou ajouter un autre nouvel article? Vous devez modifier le code existant, ajouter de nouvelles propriétés, etc.

Tout cela peut être évité avec ng-content. Il peut être considéré comme un espace réservé pour le contenu. Il affiche tout ce que vous mettez entre les balises d'ouverture et de fermeture du composant.

// button.component.ts
import {Component} from '@angular/core';

@Component({
  selector: 'app-button',
  template: `<button>
               <ng-content></ng-content>
             </button>`,
})
export class ButtonComponent {
}

// app.component.ts
import {Component} from '@angular/core';

@Component({
  selector: 'app-root',
  template: `<app-button>
               <app-icon></app-icon>
               Button
             </app-button>`,
})
export class AppComponent {
}

Code Stackblitz

Maintenant, si nous avions besoin d'un bouton d'icône, nous plaçons simplement le composant d'icône entre les balises de bouton. Vous pouvez ajouter n'importe quoi et n'importe quoi. N'est-ce pas le paradis? Notre composant de bouton est devenu flexible et beau.

Quel rôle joue l'attribut select pour le contenu ng?


Parfois, nous devons organiser du contenu à un certain endroit par rapport au reste du contenu, dans ce cas, nous pouvons utiliser un attribut selectqui accepte un sélecteur ( .some-class, some-tag, [some-attr]).

// button.component.ts
import {Component} from '@angular/core';

@Component({
  selector: 'app-button',
  template: `<button>
               <ng-content></ng-content>
               <div>
                 <ng-content select="app-icon"></ng-content>
               </div>
             </button>`,
})
export class ButtonComponent {
}

Code Stackblitz

Maintenant, l'icône est toujours affichée ci-dessous, quel que soit le reste du contenu. Perfecto!

Qu'est-ce que ngProjectAs?


L'attribut selecty ng-contentgère les balises qui sont au premier niveau d'imbrication du composant parent. Mais que se passe-t-il si nous augmentons le niveau d'imbrication d'un composant icône en l'enveloppant dans une balise?

// app.component.ts
import {Component} from '@angular/core';

@Component({
  selector: 'app-root',
  template: `<app-button>
               <ng-container>
                 <app-icon></app-icon>
               </ng-container>
               Button
             </app-button>`
})
export class AppComponent {}

Nous verrons que cela selectne fonctionne pas, comme s'il n'existait pas du tout. Cela se produit car il <ng-content select="...">recherche uniquement au premier niveau d'imbrication du contenu du parent. Il existe un attribut pour résoudre ce problème ngProjectAs. Il prend le sélecteur et «masque» tout le nœud DOM sous lui.

// app.component.ts
import {Component} from '@angular/core';

@Component({
  selector: 'app-root',
  template: `<app-button>
               <ng-container ngProjectAs="app-icon">
                 <app-icon></app-icon>
               </ng-container>
               Button
             </app-button>`
})
export class AppComponent {}

Code Stackblitz

Case * ngIf + ng-content


Examinons un autre cas intéressant. Supposons que nous devions cliquer sur le bouton icône masquer / afficher. Ajoutez une propriété booléenne à la classe de composants de bouton chargée d'afficher l'icône, modifiez-la en cliquant sur le bouton et accrochez-la ngIf.

// button.component.ts
import {Component, Input} from '@angular/core';

@Component({
  selector: 'app-button',
  template: `<button (click)="toggleIcon()">
               <ng-content></ng-content>
               <div *ngIf="showIcon">
                 <ng-content select="app-icon"></ng-content>
               </div>
             </button>`,
})
export class ButtonComponent {
  showIcon = true;

  toggleIcon() {
    this.showIcon = !this.showIcon;
  }
}

L'icône est masquée / apparaît en cliquant. Bien! Mais ajoutons quelques journaux pour les hooks OnInitet OnDestroypour le composant icône. C'est un fait bien connu qu'une directive, ngIflorsqu'une condition est modifiée, supprime / crée complètement un élément et OnDestroy/ OnInitdoit fonctionner en conséquence à chaque fois.

// icon.component.ts
import { Component, OnInit, OnDestroy } from '@angular/core';

@Component({
  selector: 'app-icon',
  template: '☻',
})
export class IconComponent implements OnInit, OnDestroy {
  ngOnInit() {
    console.log('app-icon init');
  }

  ngOnDestroy() {
    console.log('app-icon destroy')
  }
}

Code sur Stackblitz

Quelques fois, nous cliquons sur le bouton, nous nous assurons que l'icône disparaît, puis apparaît. Ensuite, nous allons à la console du développeur dans l'espoir de voir nos journaux convoités, cependant ... ils ne le sont pas!

Il n'y a qu'un seul journal pour créer un composant. Il s'avère que notre composant icône n'est jamais supprimé, mais simplement caché. Pourquoi cela arrive-t-il?

ng-content ne crée pas de nouveau contenu, il projette simplement les contenus existants. Par conséquent, le composant dans lequel le contenu est déclaré est responsable de la création et de la suppression. Pour moi, ce fut un moment totalement non évident. Corrigeons notre solution pour qu'elle fonctionne comme prévu initialement.

// button.component.ts
import {Component} from '@angular/core';

@Component({
  selector: 'app-button',
  template: `<button>
                <ng-content></ng-content>
                <ng-content select="app-icon"></ng-content>
             </button>`,
})
export class ButtonComponent {
}

// app.component.ts
import { Component } from "@angular/core";

@Component({
  selector: 'app-root',
  template: `<app-button (click)="toggleIcon()">
              <div *ngIf="showIcon" ngProjectAs="app-icon">
                <app-icon></app-icon>
              </div>
              Button
            </app-button>`,
})
export class AppComponent {
  showIcon = true;

  toggleIcon() {
    this.showIcon = !this.showIcon;
  }
}

Code sur Stackblitz

Après avoir ouvert les journaux, nous pouvons voir que le composant de l'icône est créé et supprimé comme il se doit.

Au lieu d'une conclusion


J'espère que cet article vous a aidé un peu avec la projection de contenu dans Angular.
Il est catégoriquement incompréhensible pour moi que la documentation officielle ait ignoré ce sujet. Le référentiel angulaire bloque même le problème à ce sujet depuis 2017. Apparemment, l'équipe Angular a des choses plus importantes à faire.

All Articles