Input binding: Comunicación de datos de componente principal a anidado

Esta es probablemente la forma más común y directa para compartir datos, funciona usando el decorador @Input() en las propiedades de la clase que recibe los datos para permitir el intercambio de datos usando las plantillas.

El código fuente de este ejemplo se puede encontrar en GitLab en el siguiente enlace.

Componente anidado

Añadimos un nuevo componente a nuestro proyecto:

ng generate component child

Para usar el decorador @Input() (es un interfaz) añadimos la siguiente cabecera [src/app/child/child.component.ts].

import { Input } from "@angular/core";

Este decorador define la propiedad de una clase como entrada y provee los metadatos de configuración necesarios. La propiedad así definida se asocia a una propiedad del DOM en la plantilla (usando la técnica de interpolación con "{{}}"), cuando se detecta un cambio Angular de forma automática actualiza el valor de la propiedad con el valor de la propiedad del DOM.

Añadimos a la clase un nuevo atributo con el mensaje recibido por el principal:

export class ChildComponent implements OnInit {
  @Input() childMessage: string; // <---- Nuevo atributo

  constructor() {}
  ngOnInit() {}
}

Borramos el contenido de la plantilla y añadimos en [src/app/child/child.component.html]:

<p>Say "{{ childMessage }}"</p>

He usado la técnica llamada interpolación con {{}}, es un mecanismo de Angular de sustitución de una expresión por un valor en una plantilla. Cuando Angular ve en un plantilla algo escrito entre dobles llaves {{}} lo evalúa y lo trata de convertir en una cadena, para luego volcarlo en la plantilla.

La interpolación es dinámica, si cambia el valor de la propiedad del componente, Angular cambiará su valor en todos los lugares donde se está haciendo uso de esa propiedad.

Esta sintaxis se parece a la usada por el sistema de plantillas Mustache llamado así por su forma de bigotes:

Componente principal

Definimos un nuevo nuevo atributo de la clase AppComponent [src/app/app.component.ts]:

export class AppComponent {
  title = "input-decorator";
  parentMessage = "message from parent"; // <-- Nuevo atributo
  constructor() {}
}

El componente principal carga el dependiente dentro de su plantilla, editamos y reemplazamos todo el contenido de [src/app/app.component.html]:

<app-child [childMessage]="parentMessage"></app-child>

Ejecución

ng serve --open

El resultado final:

Enlaces externos

ViewChild: De componente incrustado a principal

Enlace al proyecto.

Existe otro decorador llamado @ViewChild() que realiza el proceso inverso, permite inyectar un componente en otro facilitando al componente principal acceder a los atributos y métodos del componente anidado. El componente anidado no estará disponible hasta que se cargue, por eso debemos usar un hook con AfterViewInit para que nos notifique cuando está preparado.

Escribimos el comando ng generate component de forma abreviada:

ng g c child

Componente principal

Plantilla

Editamos la plantilla del componente principal [src/app/app.component.html] y añadimos una variable y la etiqueta <app-child> para cargar el nuevo componente.

<p>Message: {{ message }}</p>
<app-child></app-child>

Componente

Primero debemos importar algunas utilidades del core de Angular, ViewChild y AfterViewInit.

ViewChild Usamos el decorador para definir la clase hijo ChildComponent a la que queremos acceder.

import { Component } from "@angular/core";
import { ViewChild, AfterViewInit } from "@angular/core"; // <--- Nuevo
import { ChildComponent } from "./child/child.component"; // <--- Importamos componente anidado

@Component({
  selector: "app-root",
  templateUrl: "./app.component.html",
  styleUrls: ["./app.component.css"]
})
export class AppComponent implements AfterViewInit {
  // Añadimos interface AfterViewInit
  title = "viewchild";

  @ViewChild(ChildComponent) child; // <---  Decorador con clase ChildComponent

  constructor() {}

  message: string;

  /**
   * Capturamos hook cuando componente hijo está cargado
   */
  ngAfterViewInit() {
    this.message = this.child.message;
  }
}

AfterViewInit es una interfaz que implementa un método ngAfterViewInit para manejar cualquier otra tarea una vez inicializado el componente hijo.

Si seguimos la ejecución de la aplicación veremos como se llama primero al constructor de la clase principal, después al constructor correspondiente de la clase hijo, a continuación a los métodos ngOnInit de la clase principal y dependiente respectivamente y al final al método ngAfterViewInit de AppComponent.

La diferencia de las clases que extiendes con respecto a las interfaces es que las interfaces no contienen implementación de sus métodos, por lo que la clase que implementa una interfaz debe escribir el código de todos los métodos que contiene, se dice que las interfaces son como un contrato, en el que se especifica las cosas que debe contener una clase para que pueda implementar una interfaz o cumplir el contrato declarado por esa interfaz.

Esta es la definición del interfaz AfterViewInit, tratándose de una interfaz se declaransus tipos y métodos sin valor.

export interface AfterViewInit {
  /**
   * A callback method that is invoked immediately after
   * Angular has completed initialization of a component's view.
   * It is invoked only once when the view is instantiated.
   *
   */
  ngAfterViewInit(): void;
}

Componente anidado

En realidad no ha sido necesario modificar casi ni una línea del componente anidado en [src/app/child.component.ts], hemos añadido un atributo message con la cadena "Hola Mundo!" y listo.

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

@Component({
  selector: "app-child",
  templateUrl: "./child.component.html",
  styleUrls: ["./child.component.css"]
})
export class ChildComponent implements OnInit {
  message = "Hola Mundo!";

  constructor() {}

  ngOnInit() {}
}

Enlaces externos

Dependiente a principal: Compartiendo datos con @Output() y EventEmitter

Enlace al proyecto.

Otra forma de compartir datos entre componentes relacionados es emitir los datos desde el anidado, que pueden es escuchados por el principal. Esta técnica es ideal si queremos intercambiar información antes eventos determinados como un clic de ratón o el procesamiento de un formulario.

En el componente principal debemos crear una función para recibir el mensaje.

En el componente anidado declaramos una variable messageEvent con el decorador @Output y lo definimos con el emisor del evento.

Componente principal

[src/app/app.component.ts]

import { Component } from "@angular/core";

@Component({
  selector: "app-root",
  templateUrl: "./app.component.html",
  styleUrls: ["./app.component.css"]
})
export class AppComponent {
  title = "outputeventemitter";

  message: string; // <--- Nuevo atributo

  /**
   * Función que recibe evento del componente anidado
   * @param $event
   */
  receiveMessage($event) {
    this.message = $event;
  }
}

Hemos declarado un nuevo atributo de tipo cadena para recibir el mensaje del componente anidado, también hay un nuevo método que acepta como parámetro de tipo any, no hemos definido de forma explicita el tipo de variable que acepta como argumento la función por lo que se infiere que es de tipo any (existe como tipo) y no sabemos que de tipo será en el momento de escribir el programa (enlace tipos básicos en TypeScript). Se definirá el tipo en tiempo de ejecución desde fuera.

[src/app/app.component.html]

<p>Message: {{message}}</p>
<app-child (messageEvent)="receiveMessage($event)"></app-child>

En la plantilla he usado {{message}} para aplicar el binding por interpolación para mostrar el contenido de la variable definida en la clase del componente (se puede usar la misma técnica para llamar a un método también {{ metodoComponente() }}).

En la siguiente línea hemos incrustado el componente anidado identificado como <app-child> (en el selector del decorador @Component de child.component.ts). messageEvent es un atributo que hemos declarado en la clase ChildComponent, es una instancia de la clase EventEmitter y con el decorador @Output(), luego se ve más claro más abajo, la función receiveMessage es el método de la clase principal que ya hemos visto.

Componente anidado

import { Component, OnInit } from "@angular/core";
import { Output, EventEmitter } from "@angular/core";

@Component({
  selector: "app-child",
  templateUrl: "./child.component.html",
  styleUrls: ["./child.component.css"]
})
export class ChildComponent implements OnInit {
  message: string = "Hola Mundo!"; // <--- Mensaje a enviar

  @Output() messageEvent = new EventEmitter<string>();

  constructor() {}

  ngOnInit() {}

  sendMessage() {
    this.messageEvent.emit(this.message);
  }
}

Tenemos que importar en la cabecera el decorador Output (es un interfaz) y la definición de la clase EventEmitter para programar nuestros eventos personalizados (síncronos o asíncronos).

EventEmitter registra un manejador (handler en inglés) para esos eventos suscribiendo una instancia. En este caso el handler es el atributo messageEvent que dispara el evento de forma programada con el método emit con la información especificada (lo he definido como string en la declaración de la variable).

El método sendMessage() lo usaré para asociarlo a un control de tipo button de la plantilla para ver como se desencadena todo el proceso.

<button (click)="sendMessage()">Send Message</button>

Efecto final después de pulsar el botón:

Intercambio de información entre componentes no relacionados usando servicios

Enlace al proyecto.

Los servicios son la forma ideal de intercambiar información entre clases que no se "conocen" .

Creando el servicio

Creamos un nuevo servicio:

ng generate service servicio

Angular CLI genera un fichero TypeScript con el esqueleto de la declaración de una clase y el decorador @Injectable() para que forme parte del sistema de inyección de dependencias.

[src/app/servicio.service.ts]

import { Injectable } from "@angular/core";

@Injectable({
  providedIn: "root"
})
export class ServicioService {
  serviceMessage: string = "Hola mundo!"; // <--- Nuevo atributo
  constructor() {}
}

En este caso los componentes no tienen una relación de dependencia a través de las plantillas. En la clase del servicio hemos añadido una propiedad con la cadena del mensaje.

Componente

import { Component } from "@angular/core";
import { ServicioService } from "./servicio.service";

@Component({
  selector: "app-root",
  templateUrl: "./app.component.html",
  styleUrls: ["./app.component.css"]
})
export class AppComponent {
  title = "service";
  message: string;

  constructor(private servicioService: ServicioService) {}

  getServiceMessage(): void {
    this.message = this.servicioService.serviceMessage;
    console.log("getServiceMessage. this.message:" + this.message);
  }
}

Para que el servicio sea accesible he añadido la importación del mismo en la cabecera.

He añadido un parámetro privado al constructor con la clase ServicioService, este parámetro de forma simultanea define una propiedad servicioService privada de clase ServicioService inyectable.

Defino un nuevo método para obtener el mensaje del servicio.

Para activar la comunicación he tocado la plantilla del componente:

<p>{{ message }}</p>
<button (click)="getServiceMessage()">Get message from service</button>

Efecto final:

dd