martes, 12 de febrero de 2019

Angular: Suscripciones y Observables

Los observables son de los conceptos más útiles y a la vez más difíciles de entender al comenzar con Angular. Éstos se utilizan, mayormente, para compartir información entre distintos componentes de forma asíncrona.


Para estos casos Angular ya nos ofrece la opción de los eventos, pero esta forma conlleva una mayor dependencia entre componentes, lo que dificulta la reutilización del código.

Para el uso de los observables Angular se basa en el paradigma de la programación reactiva. En este caso, la comunicación se lleva a cabo a través de canales y eventos emitidos de forma asíncrona. La principal librería para la implementación de este paradigma es RxJS (Reactive Programming for JavaScript), que utiliza observables para facilitar la comunicación.

Esta librería nos ofrece diferentes implementaciones de observables. Además, incluye otras funciones útiles que nos hacen más fácil trabajar con observables. Por ejemplo, funciones para convertir el tipo y filtrar las respuestas, o convertir código existente para utilizarlo de forma asíncrona con observables.

Patrón Observador (Observer) en Angular.

El patrón observador es un patrón de diseño de software en el cual un objeto, que llamaremos sujeto (subject), gestiona una lista de suscriptores, llamados observadores (observers), y automáticamente les notifica cualquier cambio de estado.

Trabajando con Angular, nos encontraremos con tres elementos que debemos conocer:

- Subject: es el objeto que utilizaremos para emitir nuevos valores o cambios a los suscriptores.
- Observable: es el objeto que gestiona la lista de suscriptores y que notifica a éstos los cambios.
- Subscriptions: las suscripciones representan a los observadores que están a la escucha de cambios sobre el subject.

Observables en práctica.

Para que quede más claro el uso de los observables vamos a crear un nuevo proyecto para practicar esta teoría.

Comenzamos creando un nuevo proyecto:

ng new observables

Nuestro ejemplo constará de una pantalla donde mostraremos un listado de items y un contador que represente la cantidad total de estos elementos, que podremos ir creando y eliminando.

Continuaremos creando dos componentes, uno para el listado y otro para el contador:

ng g components/listado
ng g components/contador

Seguimos creando un servicio, que gestionará el listado de items:

ng g s services/listado

Añadimos los componentes y el servicio al AppModule:

// ./app/app.module.ts
import { ListadoService } from './services/listado.service';
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
import { ListadoComponent } from './components/listado/listado.component';
import { ContadorComponent } from './components/contador/contador.component';
@NgModule({
    declarations: [
        AppComponent,
        ListadoComponent,
        ContadorComponent
    ],
    imports: [
        BrowserModule
    ],
    providers: [
        ListadoService
    ],
    bootstrap: [AppComponent]
})
export class AppModule { }

Si queremos suscribirnos al mismo observable desde distintos componentes, el observable debe estar definido en forma de singleton. En caso contrario, cada componente estaría "escuchando" distintos cambios de estado. Esto ocurriría si, por ejemplo, incluyéramos el servicio en el elemento providers de los componentes, en vez de en el providers del módulo, dado que tendríamos instancias independientes del servicio.

Actualizamos el contenido de los dos nuevos componentes para representar el listado de items y el total de éstos:

// ./app/components/listado/listado.component.html
<table>
    <thead>
        <tr>
            <th>Index</th>
            <th>Nombre</th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        <tr *ngIf="!items.length">
            <td colspan="3">Sin resultados</td>
        </tr>
        <tr *ngFor="let item of items; let i = index">
            <td>#{{i}}</td>
            <td>{{item.nombre}}</td>
            <td>
                <button (click)="eliminarItem(i)">Eliminar</button>
            </td>
        </tr>
    </tbody>
</table>
<button (click)="crearItem()">Crear item</button>

// ./app/components/contador/contador.component.html
<b>Total:</b> {{contador}}

Subject, observable y subscription.

Ahora veremos cómo el servicio gestiona el listado de items y avisa a los suscriptores cuando existe algún cambio:

// ./app/services/listado.service.ts
import { Injectable } from '@angular/core';
import { Subject } from 'rxjs';

@Injectable({
    providedIn: 'root'
})
export class ListadoService {
    private items = new Array<{ nombre: string }>();
    private itemsSubject = new Subject<Array<{ nombre: string }>>();
    public itemsObservable$ = this.itemsSubject.asObservable();

    constructor() { }

    crearItem() {
        this.items.push({ nombre: 'Ejemplo ' + this.items.length });
        this.itemsSubject.next(this.items);
    }

    eliminarItem(index: number) {
        this.items.splice(index, 1);
        this.itemsSubject.next(this.items);
    }

}

Comenzamos declarando un array que contendrá el conjunto de items creados. Asimismo, declaramos un subject que utilizaremos cuando ocurra un cambio sobre el listado de items. Por último, declaramos un observable, a partir del subject, al cual nos podremos suscribir desde los componentes para estar al tanto de los cambios sobre los items.

La función "crearItem" se encarga de añadir un nuevo elemento al listado de ítems. Con el método emit del subject emitimos un aviso a los suscriptores para avisarles que el array de items ha cambiado.

Igualmente, el método "eliminarItem" elimina un ítem del array en la posición indicada. De nuevo, a través del subject avisamos a los suscriptores de este cambio.

Vamos ahora a ver cómo se gestionan estos eventos desde los componentes:

// ./app/components/contador/contador.component.ts
import { ListadoService } from './../../services/listado.service';
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs';

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

    public contador = 0;
    public subscription: Subscription;

    constructor(private listadoService: ListadoService) { }

    ngOnInit() {
        this.subscription = this.listadoService.itemsObservable$.subscribe((items: Array<{ nombre: string }>) => {
            this.contador = items.length;
        });
    }

    ngOnDestroy() {
        if (this.subscription) {
            this.subscription.unsubscribe();
        }
    }

}

Injectamos el servicio en el constructor y, en el método ngOnInit, nos suscribimos al observable que definimos en dicho servicio. Al suscribirnos a un observable obtenemos un objeto del tipo Subscription.

Al suscribimos a observables creados desde nuestros propios subjects, debemos desuscribirnos al destruir el componente. En caso contrario, seguiremos escuchando los futuros eventos emitidos por el subject, lo que puede provocar resultados inesperados.

Al suscribirnos al observable definimos la acción a realizar cuando se reciba el evento. En nuestro caso, actualizamos el contador.

// ./app/components/listado/listado.component.ts
import { ListadoService } from './../../services/listado.service';
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs';

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

    public items = [];
    public subscription: Subscription;

    constructor(private listadoService: ListadoService) { }

    ngOnInit() {
        this.subscription = this.listadoService.itemsObservable$.subscribe((items: Array<{ nombre: string }>) => {
            this.items = items;
        });
    }

    ngOnDestroy() {
        if (this.subscription) {
            this.subscription.unsubscribe();
        }
    }

    crearItem() {
        this.listadoService.crearItem();
    }

    eliminarItem(index: number) {
        this.listadoService.eliminarItem(index);
    }

}

En este caso, nos suscribimos de la misma forma al observable, salvo que ahora actualizamos el array de items.

Las funciones "crearItem" y "eliminarItem" simplemente llaman a las funciones correspondientes del servicio.

Por último, actualizamos el contenido del AppComponent para mostrar el resultado de nuestros componentes:

// ./app/app.component.html
<h2>Listado de ítems</h2>
<app-contador></app-contador>
<app-listado></app-listado>

Damos un poco de estilo a nuestra tabla de items:

// ./app/components/listado/listado.component.css
table {
    margin: 20px 0;
    border-collapse: collapse;
    width: 80%;
}
td, th {
    border: 1px solid #333;
    padding: 4px 8px;
}

Y tendríamos el siguiente resultado:


Y eso es todo.

1 comentario:

  1. Hola!!! tanto que leí en otros lados y no pude entenderlo jajaja pero acá lo logré.
    Muchas gracias!!! +1000000

    ResponderEliminar