PWA Lista de la compra - Parte II

30-08-2020 Tiempo de lectura: 9 minutos.

Volvemos con un avance de nuestra PWA

Después de un tiempo sin escribir he vuelto a disponer de un rato libre para poder dedicarle a la aplicación que estamos haciendo. He avanzado un poco en cuanto a maquetación, para no pararme mucho en esos aspectos. Como dije en el post anterior vamos a utilizar Angular Material.

Maquetación base de la pwa de la lista de la compra

Vale, ahora mismo tenemos la siguiente estructura en nuestro proyecto, ya que hemos creado unos módulos para separar un poco el código. Un módulo shared, donde tenemos el header e iremos añadiendo componentes compartidos por la aplicación. Un módulo para el componente de la lista y poco más de momento.

Imagen de estructura actual del proyecto

Para ir creando estos módulos hemos utilizado angular-cli con los siguientes comandos:

ng g module list --routing=true
ng g module shared --routing=true

Con esto tendremos dos módulos, además del módulo principal. La diferencia entre estos módulos es el routing que he agregado en el módulo list, haciendo esto, he conseguido que con el componente router-outlet que hay agregado en el html del componente principal (app.component) se muestre el componente list.

Pero no solo eso, sino que cargaremos el módulo una vez accedamos a la ruta, que como hemos puesto como ruta ‘’, nos cargará como home el list.

app.routing.module.ts

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

const routes: Routes = [
  {
    path: '',
    loadChildren: () => import('./list/list.module').then(m => m.ListModule)
  }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

list.routing.module.ts

import { ListComponent } from './list/list.component';
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

const routes: Routes = [
  {
    path: '',
    component: ListComponent
  }
];

@NgModule({
  imports: [RouterModule.forChild(routes)]
})
export class ListRoutingModule { }

Bien, vamos a entrar en faena, ya tenemos la estructura base del proyecto, tenemos una maquetación simple pero válida, así que vamos a empezar a darle funcionalidad.

En nuestro componente list, tenemos datos “fake” hasta que implementemos la introducción de nuevos artículos a la lista. Estos datos fake es un array de objetos del tipo Item. Esta clase la he creado dentro del directorio core, donde almacenaré todos los modelos y servicios que usaré en toda la app.

item.ts

export class Item {
    name: string;

    constructor(name: string){
        this.name = name;
    }
}

Con la lista de datos que vamos a utilizar como mock, lo único que tenemos que hacer para mostrarlos en nuestra web es utilizar la directiva ngFor.

list-component.html

<mat-selection-list>
    <mat-list-option *ngFor="let item of itemList">
        {{item.name}}
    </mat-list-option>
</mat-selection-list>

Ahora ya si, vamos a empezar el flujo de añadir artículos en nuestra lista de la compra. Ya tenemos el botón que abrirá un modal donde introduciremos el nombre del artículo nuevo.

Como estoy usando Angular Material, utilizaré Dialog. Para implementarlo, vamos a un componente dentro del módulo shared. Uno básico con un input y dos botones para aceptar/cancelar.

Para agregarlo directamente al módulo de shared tenemos que ejecutar el siguiente comando de angular-cli:

ng g component shared/addDialog

Tenemos que importar el módulo de Dialog en nuestro módulo, valga la redundancia.

import {MatDialogModule} from '@angular/material/dialog';

Tenemos que agregarlo en el array de entryComponents del módulo donde vamos a utilizar este componente. Con lo que nos quedaría nuestro shared.module.ts de la siguiente manera:

import { HeaderComponent } from './header/header.component';
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatIconModule } from '@angular/material/icon';
import { MatButtonModule } from '@angular/material/button';
import { MatInputModule } from '@angular/material/input';
import { AddDialogComponent } from './add-dialog/add-dialog.component';
import { MatDialogModule } from '@angular/material/dialog';
import { FormsModule } from '@angular/forms';

@NgModule({
  declarations: [
    HeaderComponent,
    AddDialogComponent
  ],
  imports: [
    FormsModule,
    CommonModule,
    MatToolbarModule,
    MatIconModule,
    MatButtonModule,
    MatDialogModule,
    MatInputModule
  ],
  exports: [
    HeaderComponent
  ],
  entryComponents: [AddDialogComponent] // <- ¡No te olvides!
})
export class SharedModule { }

Añadimos el método click a nuestro botón del header que será el que nos abra el Dialog, no quiero pararme mucho en estas cosas que son más básicas, pero si alguien tiene alguna duda puede comentarmelo por email o por twitter.

Al botón del componente addDialog que usaremos para añadir el nuevo articulo tenemos que setear el input, [mat-dialog-close], con el valor que queramos recuperar en el componente desde donde abramos el Dialog.

Aparte tenemos que ejecutar un evento para cerrar el dialog sin pasar ningún dato. Como hemos hecho antes añadimos el método click al botón y ejecutamos el siguiente código dentro de nuestro componente.

this.dialogRef.close();

¿Sencillo? Ahora vamos a recoger el valor que hemos introducido en el Dialog en el componente header y ya estaremos más cerca del propósito de este post, comunicar componentes mediante un servicio y eventos.

Para recuperar el dato solo tendremos que implementar una subcripción a uno de los eventos del Dialog, concretamente al afterClosed, evento que se dispara al cerrar el dialogo.

    dialogRef.afterClosed().subscribe(result => {
      if(result){
        // Aquí trataremos nuestro dato
      }
    });

¿Cómo comunicamos nuestros componentes?

Para esto vamos a crear un servicio dentro de nuestro módulo core. En la siguiente imagen he intentado plasmar un poco el funcionamiento del servicio que vamos a crear.

Imagen donde explico un poco la comunicación entre el header component y list component, por medio del event handler

En este servicio vamos a declarar los eventos que comunicarán nuestros componentes. Usaremos la clase EventEmitter que es una clase del core de angular.

Declarando nuestro primer evento

Bien, la clase EventEmitter hace uso de los generics (pódeis leer más aqui), con esto conseguiremos definir que tipo de dato va a “transportar” el evento.

Para crear nuestro servicio o bien lo hacemos a mano o bien usando anglar-cli:

ng g service core/services/eventHandler

Una vez creado el servicio vamos a declarar nuestro evento:

import { Item } from './../models/item.dto';
import { Injectable, Output, EventEmitter } from '@angular/core';

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

  readonly addedItem: EventEmitter<Item> = new EventEmitter();

}

Podéis observar que hemos tipado el evento addItem con la clase Item que habíamos creado, con lo que ya le hemos indicado que el tipo de dato a transportar va a ser del tipo Item.

Perfecto, ¿dudas? ¿no? Pues sigamos 🤣. Vamos a volver ahora al evento del Dialog, afterClosed. Aquí vamos a hacer varias cosas:

  1. Recuperar el dato introducido, será el nombre del item.
  2. Crear el objeto tipo Item.
  3. Emitir el evento con el item recién creado.

Como recordatorio, para que no hagáis scroll vaya a ser que os quebréis:

    dialogRef.afterClosed().subscribe(result => {
      if(result){
        // Aquí trataremos nuestro dato
      }
    });

Comprobamos que se ha introducido un valor con el if, si ha introducido un valor entoces creamos un Item. Y según tenemos definido el constructor de Item, necesitamos pasarle un parámetro que será el nombre.

const newItem = new Item(result);

A continuación este nuevo item lo vamos a “transportar” mediante el evento que hemos creado anteriormente. Necesitamos inyectar en nuestro componente el servicio de eventos.

constructor(public dialog: MatDialog, private readonly eventsHandler: EventsHandlerService) { }

Al inyectar esta dependencia ya podemos hacer uso de este servicio en nuestro componente. Asi que vamos a emitir nuestro evento.

this.eventsHandler.addItem.emit(newItem);

¡Joder! ¡Leches! ¡Parece fácil! Lo és. Así es como nos quedaría nuestro header.component

export class HeaderComponent implements OnInit {

  constructor(public dialog: MatDialog, private readonly eventsHandler: EventsHandlerService) { }

  ngOnInit() {
  }

  openAddItemDialog(){
    const dialogRef = this.dialog.open(AddDialogComponent, {
      width: '300px',
    });

    dialogRef.afterClosed().subscribe(result => {
      if(result){
        const newItem = new Item(result);
        this.eventsHandler.addedItem.emit();
      }
    });
  }

}

Ya he emitido el evento…y ahora ¿qué?

Ahora toca suscribirse a este evento, podríamos hacerlo en todos los componente que queramos. Imaginaros 3 componentes, uno igual que el nuestro, un input, y otros dós que son gráficos. Cuando añadimos algo y hay que actualizar los dos componentes, se emitiría un evento desde el componente donde se encuentra el input y los otros dos componentes que tienen gráficas se suscriben para obtener el nuevo dato del input y actualizar dichas gráficas.

No sé si con el ejemplo os he liado más u os he aclarado la cosa…

Bien vamos a esa suscripción, más barata y más fácil de suscribirte a Netflix. Al igual que en el componente anterior, debemos inyectar la dependencía del servicio y ya podremos utilizar el servicio y suscribirnos.

this.eventsHandler.addItem.suscribe((item: Item)=>{
  this.itemList.push(item);
})

Pues ya tenemos la suscripción hecha, ya solo nos quedaba añadir el nuevo evento a la lista. Borramos la lista de elementos fake que teniamos para el maquetado.

Veamos como funciona…

Gif del funcionamiento de la app en el navegador. Añadiendo un item a la lista

Recordad que el avance lo iré subiendo a mi Github, cada avance en su rama propia.

Como siempre recuerdo podéis hacerme sugerencias en Twitter, críticas, alágos o lo que queráis. Si queréis que veamos un tema en concreto, ya sea e angular u otra tecnología también puedes decírmelo.