transclusióntransclusioncomunicacióncomunicaciondompadrehijoelementrefrendererng-templateng-containerdinamicodinámicoContentChildContentChildren/@ContentChild/@ContentChildrentemplate reference variable/@ViewChildViewChildrxjsserviciorendererresolveComponentFactoryfactoryentryComponents
www.jolugama.com

Angular 7 - Comunicación entre componentes y modificación del DOM

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>

3.5 Componentes dinámicos

  • Lo primero es decir en el módulo que se renderice en caliente mediante entryComponents

src/app/app.module.ts

import { MiDinamicoComponent } from './simple-alert-view/simple-alert-view.component';
...
@NgModule({
  declarations: [
    MiDinamicoComponent
  ],
  ...
  entryComponents: [
    MiDinamicoComponent
  ],
  ...
  • Crea un div con un template reference variable en el lugar donde se va a crear dinámicamente:
<div #aqui></div>
  • En el ts añade el viewChild de tipo ViewContainerRef. Crea componentes en su interior de forma dinámica.
  @ViewChild("aqui", {read:ViewContainerRef}) aquiContainer: ViewContainerRef;
  • En el constructor, inyecta el servicio ComponentFactoryResolver, el cual permite fabricar componentes de forma dinámica.
  constructor(private renderer: Renderer2, private resolver: ComponentFactoryResolver) { 
  }
  • Crea la factoría en un método, mediante resolveComponentFactory y 'createComponent. Cuando se llame al método, creará el componente en el lugar indicado.
  public miReferencia:ComponentRef<MiDinamicoComponent> = null; // se declara una variable referencia.

  ...
  public mostrarMiComponenteDinamico(){
    const miFactory = this.resolver.resolveComponentFactory(MiDinamicoComponent);
    this.miReferencia = this.alertContainer.createComponent(miFactory);
    // acceso a propiedades públicas. da igual que sean @Input(), lo importante es que sean públicas.
    this.miReferencia.instance.title = "esto es un título";
    this.miReferencia.instance.subtitle = "esto es un subtítulo";

    // un @Output() de tipo eventEmiter. nos subscribimos desde el código
    this.miReferencia.instance.destruir.subscribe(()=>{
      this.miReferencia.destroy(); // destruimos el componente.
    });
    this.miReferencia.instance.show(); // mostramos el componente dinámico.
  }
 
... y finalmente... Si te ha gustado, difúndelo. Es solo un momento. escríbe algo en comentarios 😉 Gracias.