Articles

Publié par
Web

Il y a 3 semaines -

Temps de lecture 4 minutes

Angular 6 : Ajouter dynamiquement des composants

Imaginons une application Angular 6 (frontend) qui recevrait son contenu depuis un Content Management System (CMS), des composants sont fixés comme le Header, le Footer, ou encore le composant Hero ; le corps de la page est composé de plusieurs composants réutilisables et interchangeables.

Comment résoudre ce casse-tête de façon efficace sur une application Angular ?

Mise en pratique

Une implémentation naïve serait de boucler sur tous les composants connus de l’application, directement dans le template HTML, et d’afficher uniquement ceux qui sont effectivement présents dans les données envoyées par le CMS.

Bien que cette solution fonctionne, elle maintient un lien fort entre les composants et le composant page (via le template), les tests unitaires ne sont pas aisés et le template devient rapidement volumineux.

Reprenons notre CMS, celui-ci renvoie une liste de composants en précisant le type de chacun :

{
  "components": [
    {
      "type": "componentA",
      "content": {}
    },
    {
      "type": "componentB",
      "content": {}
    },
    {
      "type": "componentC",
      "content": {}
    }
  ]
}

L’idée est d’ajouter à notre composant principal tous ces composants dans l’ordre de la liste.

Pour ce faire il va d’abord falloir créer un composant dans lequel injecter dynamiquement les composants venant du CMS.

@Component({
  selector: 'app-root',
  template: `
    

<h1>Welcome!</h1>


  `,
})
export class AppComponent {
}

Ce composant est basique, il faut maintenant ajouter une ancre dans le template de ce composant. L’ancre sert de point d’entrée pour l’injection des composants dynamiques. C’est une directive.

@Directive({
  selector: '[anchor-factory]'
})
export class AnchorFactoryDirective {
  constructor(
    public viewContainerRef: ViewContainerRef,
  ) {
  }
}

Une fois l’ancre ajoutée, il faut créer dynamiquement les composants et les ajouter : pour cela Angular fourni le service ComponentFactoryResolver.

La méthode resolveComponentFactory de ce service permet de récupérer la factory du composant passé en paramètre.

Il suffit alors de passer cette factory à la méthode createComponent de ViewContainerRef pour qu’elle puisse construire un composant et l’ajouter à l’ancre précédemment définie.

@Component({
  ...
  template: `
    ...
    <ng-template anchor-factory></ng-template>
  `,
})
export class AppComponent implements OnInit {
  @ViewChild(AnchorFactoryDirective) anchorFactory: AnchorFactoryDirective;
  constructor(
    private componentFactoryResolver: ComponentFactoryResolver,
  ) {
  }
  ngOnInit(): void {
    // Get data from CMS
    const dynamicComponentsData: DynamicComponentData[] = [
      {type: 'componentA', content: {}},
    ];
    ...
  }
}

Une fois les données du CMS récupérées (un tableau correspondant aux données des composants à injecter), il faut maintenant parcourir ce tableau et créer tous les composants correspondant :

ngOnInit(): void {
  ...
  let factory: ComponentFactory<DynamicComponent> = null;
  for (const data of dynamicComponentsData) {
    switch (data.type) {
      case 'componentA':
        factory = this.componentFactoryResolver.resolveComponentFactory(AComponent);
        break;
      default:
        break;
    }
    if (factory) {
      const component = this.anchorFactory.viewContainerRef.createComponent(factory).instance;
      component.data = data;
    }
  }
}

 

Et voilà, à partir de cette étape l’injection de composant dynamique est automatisée.

Pour ajouter un composant il ne reste qu’à définir un nouveau composant (comme le composant A) et d’ajouter un cas au switch case.

Une amélioration consiste à faire étendre tous les composants injectables une interface qui définit un paramètre data. Ce paramètre permet de remplir le composant :

export interface DynamicComponent {
  data: DynamicComponentData;
}

export interface DynamicComponentData {
  type: string;
  content: any;
}

Bien sûr pour lier tout ça il faudra s’appuyer sur un module (qui fera le lien entre la directive, le composant principal, le service de création dynamique et les composants injectés).

 

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
import { AnchorFactoryDirective } from './anchor-factory.directive';
import { AComponent } from './a/a.component';

@NgModule({
  declarations: [
    AppComponent,
    AnchorFactoryDirective,
    AComponent,
  ],
  imports: [
    BrowserModule,
  ],
  bootstrap: [
    AppComponent,
  ],
  entryComponents: [
    AComponent,
  ]
})
export class AppModule { }


À noter que les composants dynamiques doivent apparaitre dans le tableau declarations et (surtout) dans le tableau entryComponents.

Ajouter un composant dans entryComponents permet de créer une factory, ce qui rend possible l’appel à resolveComponentFactory.

⚠️ Dans le cas d’une navigation vers ce même composant, afin que ce dernier se mette à jour en fonction des nouvelles données, l’initialisation dynamique des composants doit se faire dans la méthode ngOnChanges et non pas ngOnInit ; l’ancre doit également être vidée via la méthode clear de ViewContainerRef.

C’est terminé, nous avons vu une méthode concise, testable (extraire le code de ngOnInit dans un service, utiliser des mocks) et appuyée par Angular pour injecter dynamiquement des composants à une page via une ancre (directive) et au service ComponentFactoryResolver d’Angular.

Retrouver les sources sur le dépôt illustrant l’injection dynamique de composant sur Github.

Publié par

Publié par Benjamin Lacroix

Benjamin Lacroix est lead développeur et manager chez Xebia. Il est également formateur au sein de Xebia Training .

Commentaire

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Nous recrutons

Être un Xebian, c'est faire partie d'un groupe de passionnés ; C'est l'opportunité de travailler et de partager avec des pairs parmi les plus talentueux.