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)
- 2. Comunicación entre componentes
- 3 Acceso y manipulación del DOM
-
4. Componentes dinámicos
- 4.1 Indicar que componentes son dinámicos en el módulo
- 4.2 Indicar lugar donde debería cargar el componente dinámico
- 4.3 Fabricador de componentes dinámicos: ComponentFactoryResolver
- 4.4 Crear factoría y visualizar componente
- 4.5 Acceso a los métodos y propiedades públicos
- 4.6 Destruir componente dinámico
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 unng-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 unng-content
. Hay que usarQueryList
.
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 deng-content
, directamente en el template del padre. Es decir, es como unatemplate 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 tipoViewContainerRef
.
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);
}
escríbe algo en comentarios
😉 Gracias.