Visualización dinámica de componentes en Angular

1
18079

Índice de contenidos


1. Entorno

Este tutorial está escrito usando el siguiente entorno:

  • Hardware: Slimbook Pro 2 13.3″ (Intel Core i7, 32GB RAM)
  • Sistema Operativo: LUbuntu 18.04
  • Visual Studio Code 1.24.0
  • @angular/cli 6
  • @angular 6


2. Introducción

Cuando desarrollamos aplicaciones web con Angular nos podemos encontrar con el caso de tener que visualizar componentes de forma dinámica en base a una cierta lógica de negocio; pero no me refiero a que un componente padre renderice uno u otro hijo en función de un *ngIf, nos estamos refiriendo a instanciar el componente de forma dinámica. El componente debe existir ya.

Un caso de uso que cada vez es más habitual es en el que tenemos una tabla con registros donde cada uno tiene un tipo diferente y en función de ese tipo al hacer click sobre la fila queremos mostrar el componente que le corresponda.


3. Vamos al lío

En cualquier proyecto de Angular CLI que tengamos podemos crear nuestro componente dinámico que será el encargado de a través de las clases ComponenteFactoryResolver y ViewContainerRef instanciar la clase del componente que le indiquemos y sus inputs asociados. Para crearlo simplemente ejecutamos:

$> npm run ng -- generate component dynamic

Esto nos va a crear el fichero dynamic.component.ts donde establecemos la siguiente implementación:

import { Component, ComponentFactoryResolver, Input, ReflectiveInjector, ViewChild, ViewContainerRef } from '@angular/core';

@Component({
  selector: 'app-dynamic',
  template: ``, (1)
  styleUrls: ['./dynamic.component.css']
})
export class DynamicComponent {

  currentComponent = null;

  @ViewChild('dynamicComponentContainer', { read: ViewContainerRef }) dynamicComponentContainer: ViewContainerRef; (2)

  @Input() set componentData(data: { component: any, inputs: any }) { (3)
    if (!data) { (4)
      return;
    }

    let inputProviders = [];
    if (data.inputs) { (5)
      inputProviders = Object.keys(data.inputs)
        .map((inputName) => {
          return {
            provide: inputName,
            useValue: data.inputs[inputName]
          };
        });
    }
    const resolvedInputs = ReflectiveInjector.resolve(inputProviders); (6)

    const injector = ReflectiveInjector.fromResolvedProviders(resolvedInputs, this.dynamicComponentContainer.parentInjector); (7)

    const factory = this.componentFactoryResolver.resolveComponentFactory(data.component); (8)

    const component = factory.create(injector); (9)

    this.dynamicComponentContainer.insert(component.hostView); (10)

    if (this.currentComponent) { (11)
      this.currentComponent.destroy();
    }

    this.currentComponent = component; (12)
  }

  constructor(private componentFactoryResolver: ComponentFactoryResolver) { (13)
  }

}

  • (1) Al tratarse de un componente contenedor hacemos uso de ng-tamplate y le damos un identificador por el que poder ser recuperado.
  • (2) A través del anterior identificador y con la ayuda de la clase ViewContainerRef nos quedamos con la referencia del ng-template
  • (3) Establecemos una propiedad de entrada con @Input donde vamos a recibir un objeto que va a tener el componente a instanciar y los inputs que tenga asociados. Se va a encargar de instanciar el componente, gestionar la destrucción del mismo y establecer los inputs requeridos.
  • (4) Si no hay data directamente devolvemos el control.
  • (5) Solo es caso de que el componente a instanciar tenga inputs asociados los establecemos dentro de la variable inputProviders.
  • (6) Creamos el objeto resolvedInputs a través de inputProviders gracias a la clase RefectiveInjector que proporciona Angular.
  • (7) Creamos el objeto injector necesario para resolver las dependencias que el componente a instanciar pueda injectar a través del constructor.
  • (8) A través de la instancia del componente proporcionado se crea una factoria.
  • (9) A través de la factoria se crea el componente pasándole el objeto injector.
  • (10) Se inserta el componente creado en el área marcada con ng-template del componente contenedor.
  • (11) En caso de que ya exista un componente lo destruimos para crear uno nuevo.
  • (12) Establecemos el componente como el componente actual.
  • (13) Inyectamos a través del constructor la clase ComponentFactoryResolver necesaria para la lógica del componente contenedor.

Antes de poder utilizar este componente tenemos que tener una cosa en cuenta, y es que los componentes que vayamos a visualizar de forma dinámica al no poner explícitamente su selector en ningún sitio necesitan ser declarados en la propiedad entryComponents (a parte de declarations) del @NgModule correspondiente.

Ahora en cualquier template de cualquier componente podemos hacer uso del componente contenedor de esta forma:

$<app-dynamic [componentData]="componentData">$</app-dynamic>

Y en la lógica de ese componente establecer un valor para componentData, pasándole los inputs del componente si es que los tuviera.

componentData: any = {
    component: HomeDefaultComponent,
    inputs: {
      name: 'dinámico'
    }
  };

Y para recuperar el input «name» establecido, no podemos hacerlo con @Input, sino que tenemos que hacer uso de la clase Injector de Angular, de este forma:

import { Component, Injector, OnInit } from '@angular/core';

@Component({
    selector: 'home-default',
    template: ` 

Name: {{name}}

` }) export class HomeDefaultComponent implements OnInit { name: string; constructor(private injector: Injector) {} ngOnInit() { this.name = this.injector.get('name'); } }


4. Conclusiones

Como véis le hemos dado una solución muy sencilla y elegante a un caso de uso que sin esta implementación podría volverse inmanejable simplemente conociendo y exprimiendo un poco este framework tan poderoso.

Recordad que esta técnica y otras muchas más las encontraréis en la guía práctica de Angular y también ofrecemos cursos in-house y online.

Cualquier duda o sugerencia en la zona de comentarios.

Saludos.

1 COMENTARIO

  1. Exelente trabajo, organizado, limpio y facil. Gracias.
    Tengo una pregunta, que deberia agregar o modificar si necesito que los componenetes creados dinamicamente me retornen valores utilizando los @Output.

    Agradeceria tu ayuda.

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad