Actualmente se desarrollan aplicaciones cada vez más complejas, con mucho contenido y diferentes pantallas. Estas aplicaciones se crean montando componentes más pequeños y especializados. Angular nos facilita su desarrollo pudiendo separar grandes funcionalidades en componentes pequeños e independientes.

Gracias al uso de componentes se logra un código más legible y mantenible, pero es necesario lograr una eficiente comunicación entre los mismos, ya que se empieza a tener una gran cantidad de componentes a medida que la aplicación crece.

En estos casos, es normal que nos surjan una serie de dudas: ¿estoy separando bien estos componentes?, ¿la comunicación entre ellos es la correcta?, ¿dónde debería tratar los datos?

En este post vamos a hablar sobre el patrón contenedor/presentadores que ayudará a resolver estas preguntas que surgen al desarrollar con este framework basado en componentes.

Introducción: Comunicación entre componentes

Es importante conocer cómo se pueden comunicar los componentes entre sí, dependiendo de la situación en la que nos encontremos.

Comunicación entre componentes acoplados

Al desarrollar una nueva pantalla, por lo general utilizamos varios componentes que comparten el mismo contenedor (padre) que facilita la comunicación entre ellos. Para realizar esta comunicación aplicaremos el patrón contenedor/presentadores.

Comunicación entre componentes en páginas diferentes

{
  path: 'detail/:id',
  component: HeroDetailComponent
}

La comunicación entre páginas diferentes se realiza mediante el routing, así pasamos parámetros de una a otra.

Comunicación entre componentes entre estructuras dinámicas

//Create observable
const myObs = from ('Hello world!');

//Suscribe observable
const subscription = filteredObs.subscribe(char => console.log(char))

Cuando se necesita comunicar componentes o servicios desacoplados, la comunicación se hace más complicada. Se realiza mediante observables.

Gracias a los observables podemos usar un almacén de datos y, cuando se modifique algún dato de dicho almacén, recibir automáticamente los cambios, sin tener que programar a mano ese tránsito de la información.

Patrón Contenedor/Presentadores

La idea principal de este patrón es que la responsabilidad de obtener y manipular el modelo se centralice en el Contenedor. Los Presentadores recibirán la información desde el Contenedor y la presentarán al usuario, esperando su reacción. Cuando ocurra, lo notificarán de vuelta al Contenedor padre que interactúa con el modelo.

Seguir este patrón es bastante sencillo en la práctica, pero requiere un trabajo previo antes de empezar a desarrollar. Hay que estructurar bien la pantalla, identificar qué componentes se van a usar y qué requisitos tienen. Realizar este trabajo previo lleva un tiempo pero ahorrará mucho trabajo en el desarrollo y posibles modificaciones futuras.

En Angular todo son componentes, pero utilizando este patrón hay que diferenciar entre:

Contenedores

Estos componentes principales son los encargados de obtener datos, preparar los datos necesarios para los presentadores, aplicarles lógica de negocio y guardarlos cuando corresponda. Además de esto, el contenedor puede estar preparado para recibir eventos de los presentadores.

Por lo tanto, tendremos una vista muy sencilla y un controlador más complejo. La vista será la composición de los componentes presentadores.

En el caso de tener que interactuar con un servidor, el contenedor será el encargado de llamar al servicio correspondiente. Las llamadas a las APIs nunca se declaran en el componente, siempre se deben realizar a través de un servicio.

Presentadores

La función de estos subcomponentes es mostrar el contenido que le envía el componente principal y controlar la funcionalidad propia del subcomponente. Además, estos subcomponentes pueden estar preparados para recibir parámetros del componente padre que le indican cómo tiene que comportarse.

Es importante entender que estos subcomponentes tienen que ser totalmente independientes y no conocer ninguna lógica de la aplicación, de tal manera que estos podrían ser utilizados en cualquier aplicación sin tener que adaptar nada.

Muy importante no realizar llamadas a servicios en los presentadores ya que de esta manera el componente dejará de cumplir este patrón y no será reutilizable.

Comunicación entre Contenedores y Presentadores

La comunicación entre contenedor y presentador se pueden dar en dos sentidos:

Contenedor-Presentador

En este caso el contenedor pasa información al presentador. Esta información no es únicamente contenido que el presentador va a mostrar, sino también configuración que usará el presentador para actuar de una manera determinada.

Para realizar esta comunicación se utiliza el decorador @Input.

Los pasos para realizar esta comunicación en Angular son:

  1. Se declarara el decorador @Input en el componente hijo asignándole una variable, de tal manera que en dicha variable estará la información que le llega del componente padre.
import { Component, OnInit, Input } from '@angular/core';

@Component({
  selector: 'app-presentador',
  templateUrl: './presentador.component.html',
  styleUrls: ['./presentador.component.css']
})
export class PresentadorComponent implements OnInit {

  @Input() infoContenedor: string;

  constructor() { }

  ngOnInit() {
  }

}
ngOnInit () {
    console.log(this.infoContenedor)
  }
<div>
  <h3>HTML presentador</h3>
  
    Todo lo que está contenido dentro de este div está declarado en el html correspondiente al elemento
    presentador.component.html.
    El siguiente contenido está pasado desde el padre, gracias a la declaración @Input.:
    <strong>{{infoContenedor}}</strong>
  
</div>
  1. Para que el hijo reciba el dato del padre es necesario que el padre lo envíe a través de su HTML, donde se hace uso de los componentes hijos, mediante la variable declarada en el hijo con el decorador @Input.
<app-presentador [infoContenedor]="’string’"></app-presentador>
import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'app-contenedor',
  templateUrl: './contenedor.component.html',
  styleUrls: ['./contenedor.component.css']
})
export class ContenedorComponent implements OnInit {

  datoComunicar: string;

  constructor() { }

  ngOnInit() {
  }

  realizarComunicacion(dato: string) {
    this.datoComunicar = dato;
  }

}
<div class="container">
  <h1>HTML Contenedor</h1>
  
    Todo lo que está contenido dentro de este div está declarado en el html correspondiente al elemento
    contenedor.component.html
  
  <app-presentador [infoContenedor]="datoComunicar"></app-presentador>
</div>

Presentador-Contenedor

En este caso el presentador pasa información al contenedor. Esta comunicación es muy utilizada cuando se quiere avisar al presentador de alguna acción que ha sucedido.
Para realizar esta comunicación se utiliza el decorador @Output.

Los pasos para realizar esta comunicación en Angular son:

  1. Se declara un decorador @Output que es de tipo EventEmitter() en la clase ts del presentador.
  2. Se crea un método que será llamado cuando se quiera realizar la comunicación.
import { Component, OnInit, Output, EventEmitter } from '@angular/core';

@Component({
  selector: 'app-presentador',
  templateUrl: './presentador.component.html',
  styleUrls: ['./presentador.component.css']
})
export class PresentadorComponent implements OnInit {

@Output() eventoComunicar = new EventEmitter();

  constructor() { }

  ngOnInit() {
  }

  realizarComunicacion(dato: string){
    this.eventoComunicar.emit({elemento: dato});
  }
 }
  1. En el archivo html del contenedor, dentro de la etiqueta asociada al elemento hijo, se declara un evento con el mismo nombre del decorador @Output declarado en el punto uno, y se iguala a un método que se deberá crear en la clase asociada al componente padre. Este método recibe como parámetro un evento ($event) que es aquello emitido por el EventEmitter del hijo.

<app-presentador
      (eventoComunicar)="realizaComunicacionHijo($event)"
>
</app-presentador>
  1. En el método creado en el punto anterior, se recuperarán todos los datos.
import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'app-contenedor',
  templateUrl: './contenedor.component.html',
  styleUrls: ['./contenedor.component.css']
})
export class ContenedorComponent implements OnInit {

  datoComunicarPadre: string;

  constructor() { }

  ngOnInit() {
this.realizarComunicacionHijo();
  }

  realizaComunicacionHijo(event) {
    this.datoComunicarPadre = event.elemento;
  }

}

Ejemplo práctico

Para entender mejor este patrón vamos a ver un ejemplo de cómo se aplica a un caso práctico de uso.

La siguiente pantalla se trata de un listado de personas a las que se le puede enviar una encuesta para que voten sus preferencias. Este listado está ordenado alfabéticamente.

Análisis

Lo primero es distinguir el contenedor y los presentadores. El contenedor contiene un texto y un listado. El texto no es necesario separarlo en un componente ya que muy simple, pero el listado si lo separaremos en un componente.

Vamos a analizar qué funciones tiene que realizar cada uno:

Contenedor:

Presentador:

Para asegurarse de que estamos definiendo bien los subcomponentes hay que plantearse si este podría ser usado en una aplicación completamente diferente. En nuestro caso se trata de un listado con un botón, pero este no tiene conocimiento de que es lo que está mostrando ni la acción que desempeña el botón.

Además de definir el contenedor y el presentador siguiendo el patrón, hay que definir el servicio que tenemos que utilizar para obtener los amigos y guardar los votos. Ningún componente debe hacer llamadas a las APIs, son los servicios los que se tienen que ocupar de esto. Gracias a esto los componentes son más independientes, que es nuestro objetivo principal.

Comunicación

Código Contenedor

import { Component, OnInit } from '@angular/core';
import { VoteService } from '../vote.service';

@Component({
  selector: 'app-vote',
  templateUrl: './vote.component.html',
  styleUrls: ['./vote.component.css']
})
export class VoteComponent implements OnInit {

  constructor (private voteService: VoteService) { }

  allFriends: string[];

  ngOnInit (): void {
    this.getFriends();
  }

  getFriends () {
    this.voteService.getFriends().subscribe(friends => {
      this.allFriends = friends;
    })
  }

  sendVote (item) {
    this.voteService.saveVote(item).subscribe(friends => {
      alert('Guardado correctamente')
    })
  }

}
<h1>Envia la encuesta para que la voten tus amigos</h1>
<app-list 
    [items]='allFriends' 
    [buttonText]="'Enviar'" 
    (clickItem)="sendVote($event)"

Código Presentador

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

@Component({
  selector: 'app-list',
  templateUrl: './list.component.html',
  styleUrls: ['./list.component.css']
})
export class ListComponent {

  constructor () { }

  @Input() items: string[];
  @Input() buttonText: string;

  @Output() clickItem = new EventEmitter;

  propagationClick (item) {
    this.clickItem.emit(item)
  }

}
<ul>
  <li *ngFor="let item of items">
    <span>{{item}}</span>
    <button (click)="propagationClick(item)">{{buttonText}}</button>
  </li>
</ul>

Código Servicio

import { Injectable } from '@angular/core';
import { of, Observable } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class VoteService {

  constructor () { }

  getFriends (): Observable<string[]> {
    //Simulación llamada API
    const friends = ['Maria', 'Juan', 'Elena', 'Pepe', 'Carlos']
    return of(friends)
  }

  saveVote (vote): Observable<boolean> {
    //Simulacion llamda API, devuelve que se ha guardado correctamente el voto
    return of(true)
  }
}

Conclusiones

Como ya sabemos lo más importante para un buen proyecto es seguir una buena arquitectura, así que es importante conocer patrones a seguir que hagan nuestro código más sencillo y legible.

En este caso, hemos visto un ejemplo muy sencillo de cómo aplicar este patrón. Puede parecer que no tiene gran importancia utilizarlo, pero imagina una pantalla con muchas tabs, cada una con sus propios contenidos y llamadas a servicios, siempre será necesario un contenedor que gestione todo el contenido y la lógica de negocio y unos componentes muy independientes que únicamente saben mostrar contenido.

¡Espero haberos animado a seguir haciendo un código limpio y sencillo!

Cuéntanos qué te parece.

Los comentarios serán moderados. Serán visibles si aportan un argumento constructivo. Si no estás de acuerdo con algún punto, por favor, muestra tus opiniones de manera educada.

Suscríbete

Estamos comprometidos.

Tecnología, personas e impacto positivo.