angular comunicacion componentesangular transclusióncomunicacióndompadrehijoelementrefrendererng-templateng-containerdinamicodinámicoContentChildContentChildren/@ContentChild/@ContentChildrentemplate reference variable/@ViewChildViewChildrxjsserviciorendererresolveComponentFactoryfactoryentryComponentsViewContainerRefComponentFactoryResolverinstancecreateComponentresolveComponentFactory
jolugama.com

Angular 7 - Comunicación entre componentes, modificación del DOM y componentes dinámicos

Guía de buenas prácticas de comunicación entre componentes. Transclusión (ng-content), elementRef, componentes dinámicos, @ViewChild, @ContentChild, BehaviorSubject, Subject, Renderer2 …

1. Transclusión con ng-content y select (slots)

Inclusión de un componente o parte del mismo dentro de otro componente. En AngularJs lo conocemos por ngTransclude. Un ejemplo claro es un componente modal que tiene unos estilos definidos, un título header, un texto body y una zona donde hay un número x de botones.

De esta manera nos ahoramos Outputs, eventEmitter y un complicado componente de condicionales para todas las casuísticas del proyecto, dejando que el componente padre que llame a dicho componente modal.

En este caso se usa select con atributos [], aunque se puede usar mediante clases . o como un tag directamente.

Componente modal

<div>
  <div class="background"></div>
  <div class="card">
    <div class="header">
      <ng-content select="[header]"></ng-content>
    </div>
    <div class="message">
      <ng-content select="[message]"></ng-content>
    </div>
    <div class="footer">
      <ng-content select="button"></ng-content>
    </div>
  </div>
</div>

Desde un componente padre, llama a la modal y dentro añade su propio header, body y botones. En este caso los estilos los maneja el padre, aplicando unos estilos diferentes a los definidos desde el componente modal.

Componente padre

  <app-mi-modal [hidden]="!isModalVisible">
    <div header>Esto es un título</div>  
    <div message><label>Este es el mensaje</label></div>   
    <input [(ngModel)]="numero" type="number" #numeroInput> <!-- template reference variable -->
    <button (click)="cancelarModal()" class="cancel-button">Cancelar</button>
    <button (click)="aceptarModal()" class="ok-button">Aceptar</button>
  </app-mi-modal>

2. Comunicación entre componentes

Para la comunicación entre distintos componentes, hay que seguir distintas estrategias según las posiciones entre ellos. Por ello no es lo mismo tratar un componente padre a un hijo, y viceversa.

A parte de los property binding mediante decorador @Input, event binding mediante decorador @Output, y los servicios con provider en módulo, angular nos proporciona una serie de técnicas que optimizan mucho el código.

2.1. Comunicación hijo a padre

  • Mediante inyección de dependencias. El padre dispone de unos métodos públicos y estos pueden ser usados desde el hijo. No es muy recomendable esta opción, mejor usar comunicación de padre a hijo mediante @ContentChild o @ContentChildren.

Componente padre

export class FatherComponent {

  constructor() { }

  suma(num1: number, num2: number): number {
    return num1 + num2;
  }

  resta(num1: number, num2: number): number {
    return num1 - num2;
  }

Componente hijo

import { FatherComponent } from '../FatherComponent/FatherComponent.component';

...

export class ChildrenComponent {

  constructor(public father: FatherComponent) { }

  miMetodo() {
    const a = this.father.suma(2, 3);
    const b = this.father.resta(2, 3);
  }

2.2. Comunicación padre a hijo

  • mediante atributos con decoradores @Input y @Output.
  • mediante template reference variable (desde html)

Permite referenciar un componente hijo, y llamarlo desde el propio template, teniendo acceso a todos sus métodos públicos.

<app-mi-componente #micomponente></app-mi-componente>
<button (click)="micomponente.metodoPublico()">mi botón</button>
  • mediante @ContentChild. con componente dentro de un ng-content. Usar ContentChild para obtener el primer elemento o la directiva que coincida con el selector del contenido DOM. Si el contenido del DOM cambia y un nuevo elemento secundario coincide con el selector, la propiedad se actualizará.

Es más potente que template reference variable, ya que permite acceder aparte del template, desde el propio typescript, pudiendo usar los métodos públicos del hijo desde el padre.

Importante No se puede tener @ContentChild desde el padre y inyección de dependencia desde el hijo al padre, la cual hace una dependencia circular, la cual no se puede resolver.

El contenido de un componente no está disponible para su padre hasta despues de su inicialización. Es por ello que se debe de utilizar el evento de ciclo de vida ngAfterContentInit();

  • mediante @ContentChildren si hay varios componentes iguales y están dentro de un ng-content. Hay que usar QueryList.

En el ts

export class FatherComponent implements OnInit, AfterContentInit, OnDestroy {

  @ContentChildren(ChildComponent) public myChildren:QueryList<ChildComponent>;

  ngAfterContentInit(){
    // llamar a los métodos de myChildren 
  }

En el html

<div class="content">
  <ng-content></ng-content>
</div> 
  • mediante @ViewChild. Es como @ContentChild pero sin estar dentro de ng-content, directamente en el template del padre. Es decir, es como una template reference variable pero del lado del typescript.

  • mediante @ViewChildren pero para varios componentes iguales.

2.3. Emitiendo eventos desde un servicio

Cuando aumenta la lógica de un componente, a menudo la trasladamos a un servicio. Si se tiene un eventEmitter, desde el servicio no se puede hacer de la misma manera. Hay que crear un observable y desde el componente subscribirse para pasar el eventEmitter.

  • Si se va a emitir sin valor alguno:

servicio

// Subject es un tipo especial de observable que permite que los valores se compartan a muchos observadores.
private clickadoSource = new Subject<void>(); 
public clickObs$ = this.clickadoSource.asObservable();

...

private clickar() {
  this.clickadoSource.next();
}
  • Si se emite aportando un valor:
// BehaviorSubject es como Subject, pero además se le envía un parámetro.
private numeroSource = new BehaviorSubject<number>(55);
public cambiaNumObs$ = this.numeroSource.asObservable();

...

private cambiaNum() {
  // BehaviorSubject tiene métodos, entre ellos el de recuperación de valor
  this.numeroSource.next(this.numeroSource.getValue()+1); 
}

Componente que llama al servicio


this.aSubscription = this.miServicio.clickObs$
  .subscribe(()=>{
    this.miEventEmmiter.emit();
});

this.bSubscription = this.miServicio.cambiaNumObs$
  .subscribe((data)=>{
    console.log(data);
    this.miEventEmmiter.emit(data);
});

3 Acceso y manipulación del DOM

3.1. Mediante elementRef

El elementRef es una referencia que se obtiene a partir de un elemento generado con viewChild o contentChild. Una vez creada la ViewChild de tipo ElementRef, se puede manipular el dom desde el método ngAfterViewInit.

<input [(ngModel)]="time" type="number" #miInput>
@ViewChild("miInput") nombre: ElementRef;

  ngAfterViewInit(){
    console.log(this.nombre); // aparece la propiedad nativeElement y dentro una gran cantidad de propiedades y métodos
    this.nombre.nativeElement.setAttribute('placeholder', 'Escriba su nombre');
    this.nombre.nativeElement.addClass('una-clase');
    this.nombre.nativeElement.focus();
  }

3.2. Mediante Renderer

Angular es platform agnostic, permite renderizar en varias plataformas que no sea navegador web, como es el caso de los Web Workers. Es por ello que se debe evitar a toda costa el uso de las variables globales window, document, o manipular el DOM con ElementRef.

  constructor(private renderer: Renderer2) { 
  }

  ngAfterViewInit(){
    this.renderer.setAttribute(this.nombre.nativeElement, "placeholder", "Escriba su nombre");
    this.renderer.addClass(this.nombre.nativeElement, 'una-clase');
    this.renderer.selectRootElement(this.nombre.nativeElement).focus();
  }

3.3. Directiva ng-template

Mediante este etiqueta, agrupa código html que se puede reutilizar sucesivamente.

Una forma

<div *ngIf="datos else loading">
  ... 
</div>

<ng-template #loading>
    <div>Loading...</div>
</ng-template>

Otra forma

<ng-template [ngIf]="datos" [ngIfElse]="loading">
   <div class="una-clase">
     ... 
   </div>
</ng-template>

<ng-template #loading>
    <div>Loading...</div>
</ng-template>

Sin utilizar directivas *ngIf, mediante directiva ngTemplateOutlet:

<ng-template #saluda>
    <div>Hola!!!</div>
</ng-template>

<ng-container [ngTemplateOutlet]="saluda"></ng-container>
<ng-container [ngTemplateOutlet]="saluda"></ng-container>
<ng-container [ngTemplateOutlet]="saluda"></ng-container>

3.4. Directiva ng-container

Al poner un *ngIf se crea en el dom un div inecesario. Actua como un nodo del DOM pero que en realidad no se renderiza. La forma de poder evitarlo es así.

<ng-container *ngIf="visible">
  <div class="una-clase"></div>
  ...
  </div>
</ng-container>

4. Componentes dinámicos

4.1 Indicar que componentes son dinámicos en el módulo

Esto se hace desde el módulo donde se hacen las declarations. Aclaro, si por ejemplo tenemos en el módulo shared el componente donde va a cargar x componentes dinámicos, sería shared.module.ts. Se debe añadir a entryComponents todos los componentes dinámicos

declarations: [
    miDinamicoComponent,
    miDinamicoComponent2
],
imports: [
    ...
],
entryComponents: [
    miDinamicoComponent,
    miDinamicoComponent2
]

De esta manera, angular sabe que en algún momento este componente se va a incluir al dom de forma dinámica.

4.2 Indicar lugar donde debería cargar el componente dinámico

Justo donde se quiere añadir dicho componente dinámico, añadir una template reference variable para tener una referencia al elemento.

En el html:

...
<ng-container #componenteDinamico></ng-container>

Podría ser un div, pero se crearía un padre div estuviera creado o no dicho componente dinámico. Para eliminar ese div innecesario.

Todo lo demás se hace en el ts.

  • Importa los componentes a referenciar.
  • Crea una referencia a este elemento con ViewChild de tipo ViewContainerRef.

En el ts:

export class ItemsListComponent implements OnInit, OnDestroy, AfterViewInit {
  miFactory: ComponentFactory<any>;
  componentRef: ComponentRef<miDinamicoComponent> = null; // se declara una variable referencia.
  componentRef2: ComponentRef<miDinamicoComponent2> = null; // se declara una variable referencia.
  // ViewContainerRef crea componentes en su interior de forma dinámica
  // Hay que añadir read: ViewContainerRef, ya que si no, devolvería un ElementRef,
  // que es lo que devuelve por defecto un viewChild.
  // la referencia que tenemos es compDynamicContainer, y en ese contenedor añadiremos los componentes dinámicos
  @ViewChild('componenteDinamico', { read: ViewContainerRef }) compDynamicContainer: ViewContainerRef;

4.3 Fabricador de componentes dinámicos: ComponentFactoryResolver

Insertar el servicio ComponentFactoryResolver, el cual permite fabricar componentes de forma dinámica

En el constructor:

  constructor(
    ...
    private resolver: ComponentFactoryResolver
  ) { }

4.4 Crear factoría y visualizar componente

Crea una factoría para el componente dinámico, que permita instanciarlo en el elemento contenedor creado anteriormente (compDynamicContainer).

  ngAfterViewInit() {
    this.miFactory = this.resolver.resolveComponentFactory(miDinamicoComponent);
    this.componentRef = this.compDynamicContainer.createComponent(miFactory);
  }

En este punto el componente se ha creado y se está visualizando. Si se quiere cargar en otro momento, incorporarlo en una función en vez de ngAfterViewInit.

4.5 Acceso a los métodos y propiedades públicos

Una vez guardada la referencia de la creación del componente, se puede acceder a dicho componente, ejecutar métodos públicos y acceder a sus propiedades.

Todo esto gracias a instance.

this.componentRef.instance.miFuncionSuma(15,2)
this.componentRef.instance.titulo='Cambio el titulo del componente';
this.componentRef.instance.mensaje='cambio el mensaje también';

También se puede acceder e incorporar en un @Input, ya que es público. En el caso de los @Output, nos podemos subscribir a ellos:

this.componentRef.instance.miEventEmiter(()=>{
  console.log('se ejecuta el event emiter');
});

4.6 Destruir componente dinámico

Supongamos que tenemos un contenedor compDynamicContainer, y queremos que en un primer momento se pinte un componente, pero que a los x segundos, o al pulsar cualquier acción, se elimine, o se cambie por otro componente. Esto puede ser si el componente dinámico es un modal, o tenemos un sistema de paginado con componentes dinámicos.

Esto se hace con destroy(). Un ejemplo:

ngAfterViewInit() {
    this.miFactory = this.resolver.resolveComponentFactory(miDinamicoComponent);
    this.componentRef = this.compDynamicContainer.createComponent(this.miFactory);
    this.componentRef.instance.openCard(24);

  // en 2 segundos destruimos el componente 1 e incorporamos el componente 2.
    setTimeout(() => {
      this.componentRef.destroy();
      this.miFactory = this.resolver.resolveComponentFactory(miDinamicoComponent2);
      // En la referencia componentRef, creamos el componente dinámico dentro del contenedor compDynamicContainer
      // el tipo de componente dinámico lo asigna la factoría
      this.componentRef2 = this.compDynamicContainer.createComponent(this.miFactory);
    }, 2000);
  }
 
... y finalmente... Si te ha gustado, difúndelo. Es solo un momento. escríbe algo en comentarios 😉 Gracias.