Reutiliza componentes. Interacción entre componentes usando @Input() y EventEmmiter<T> con Angular 8.
Demo: GLTrack 3ra parte. Una aplicación para monitorear niveles de glucosa para pacientes de diabetes.
Introducción
En el post de hoy haremos una nueva actualización a la aplicación GLTrack, que hemos venido construyendo y que tiene como objetivo servir para registrar y monitorear un registro personal de niveles de azúcar en la sangre para pacientes de diabetes.
Me han comentado que los posts anteriores pueden ser demasiado largos y no tan fáciles de entender. Voy a tratar de mantener los posts más breves y al mismo tiempo explicarlos mejor. Aunque creo que este post tampoco quedó tan corto, espero que si esté más claro.
NOTA: Esta es la parte 3 de 4 de la serie de posts "GLTrack"
- GLTrack parte 1: "Schematics, Componentes y Ruteo con Angular + Material".
- GLTrack parte 2: "Modelos, servicios, dependency injection, formas y data binding con Angular + Material".
- GLTrack parte 3: "Reutiliza componentes. Interacción entre componentes usando @Input() y EventEmmiter con Angular 8".
- GLTrack parte 4: "Gráficas con ng2-charts (Chart.js) y Angular 8. Ordena, filtra, mapea y reduce arreglos de objetos.".
En el post anterior, creamos un formulario para registrar mediciones e implementamos una tabla para mostrar las capturas.
Imagina que GLTrack ya está siendo usado por un usuario final, Diana. Diana entra a la aplicación y registra sus niveles de glucosa. Las mediciones se muestran en la tabla en la página de registro. Pero la aplicación sería mucho más útil para Diana si le mostrara sus datos de una manera más útil.
Hoy comenzaremos a modificar el Dashboard, la página principal de la aplicación. Lo primero que haremos es agregar al dashboard una tabla igual a la que tenemos en la página de registro.
Paso 1: Crear nuevo componente
La manera incorrecta de agregar la tabla de registro a la página principal sería copiar el código de la tabla de la página de registro y pegarlo en el componente Dashboard. En cambio, lo que vamos a hacer es crear un componente reutilizable que podamos utilizar en la página de registro y en el dashboard sin tener código duplicado en la aplicación.
Comenzaremos creando un componente nuevo desde la consola:
ng generate component components/tabla-registros
Ahora, inserta el nuevo componente en la página de registro:
<h1>Niveles de glucosa</h1>
<mat-card>
...
</mat-card>
<mat-card>
<mat-card-title>Mis registros</mat-card-title>
<app-tabla-registros></app-tabla-registros>
...
</mat-card>
Paso 2: Mover y adecuar el código de la tabla en el nuevo componente
Código HTML
Comenzamos moviendo el código HTML de la tabla desde "registro.component.html" hacia "tabla-registros.component.html".
registro.component.html:
<h1>Niveles de glucosa</h1>
<mat-card>
...
</mat-card>
<mat-card>
<mat-card-title>Mis registros</mat-card-title>
<app-tabla-registros></app-tabla-registros>
<!- El codigo estaba aqui ->
</mat-card>
tabla-registros.component.html:
<div class="row">
<mat-form-field class="row-item grow">
<input matInput (keyup)="applyFilter($event.target.value)" placeholder="Buscar">
</mat-form-field>
</div>
<table mat-table [dataSource]="dataSource" matSort>
<ng-container matColumnDef="fecha">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Fecha </th>
<td mat-cell *matCellDef="let row"> {row.Fecha|date:'medium'} </td>
</ng-container>
<ng-container matColumnDef="nivel">
<th mat-header-cell *matHeaderCellDef mat-sort-header> ID </th>
<td mat-cell *matCellDef="let row"> {row.Nivel} </td>
</ng-container>
<ng-container matColumnDef="comida">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Comida </th>
<td mat-cell *matCellDef="let row"> {row.Comida} </td>
</ng-container>
<ng-container matColumnDef="antesDespues">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Antes/Después </th>
<td mat-cell *matCellDef="let row"> {row.AntesDespues} </td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef> </th>
<td mat-cell *matCellDef="let row">
<button mat-icon-button (click)="Eliminar(row.Id)">
<mat-icon>delete</mat-icon>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
<mat-paginator [pageSizeOptions]="[5, 10, 25, 100]"></mat-paginator>
Código Typescript
Ahora continuamos moviendo el código de la tabla desde el archivo registro.component.ts hacia tabla-registros.component.ts. Para más claridad, a continuación, se muestra el código comentado en registro.component.ts:
import { Component, OnInit, ViewChild } from '@angular/core';
import { MatTableDataSource, MatPaginator, MatSort } from '@angular/material';
import { MedicionGlucosa } from 'src/app/models/models';
import { MedicionesService } from 'src/app/services/mediciones.service';
@Component({
selector: 'app-registro',
templateUrl: './registro.component.html',
styleUrls: ['./registro.component.css']
})
export class RegistroComponent implements OnInit {
registros = [] as MedicionGlucosa[];
nuevoRegistro = {} as MedicionGlucosa;
// displayedColumns: string[] = ['fecha', 'nivel', 'comida', 'antesDespues', 'actions'];
// dataSource: MatTableDataSource<MedicionGlucosa>;
// @ViewChild(MatPaginator, { static: true }) paginator: MatPaginator;
// @ViewChild(MatSort, { static: true }) sort: MatSort;
constructor(private mediciones: MedicionesService) {
}
ngOnInit() {
this.registros = this.mediciones.GetAll();
// this.dataSource = new MatTableDataSource(this.registros);
// this.dataSource.paginator = this.paginator;
// this.dataSource.sort = this.sort;
}
Guardar() {
if (!this.IsInvalid()) {
this.mediciones.Create(this.nuevoRegistro);
this.LimpiarForma();
this.registros = this.mediciones.GetAll();
// this.dataSource.data = this.registros;
}
}
Eliminar(id: string) {
this.mediciones.Delete(id);
this.registros = this.mediciones.GetAll();
// this.dataSource.data = this.registros;
}
LimpiarForma() {
this.nuevoRegistro = {} as MedicionGlucosa;
}
IsInvalid() {
return this.nuevoRegistro.Nivel === undefined
|| this.nuevoRegistro.Comida === undefined
|| this.nuevoRegistro.AntesDespues === undefined
|| this.nuevoRegistro.Comida === undefined
|| this.nuevoRegistro.Fecha === undefined;
}
// applyFilter(filterValue: string) {
// this.dataSource.filter = filterValue.trim().toLowerCase();
// if (this.dataSource.paginator) {
// this.dataSource.paginator.firstPage();
// }
// }
}
Puedes eliminar las líneas comentadas una vez que las pegas en "tabla-registros.component.ts":
import { Component, OnInit, ViewChild } from '@angular/core';
import { MedicionGlucosa } from 'src/app/models/models';
import { MatTableDataSource, MatPaginator, MatSort } from '@angular/material';
@Component({
selector: 'app-tabla-registros',
templateUrl: './tabla-registros.component.html',
styleUrls: ['./tabla-registros.component.css']
})
export class TablaRegistrosComponent implements OnInit {
registros = [] as MedicionGlucosa[];
displayedColumns: string[] = ['fecha', 'nivel', 'comida', 'antesDespues', 'actions'];
dataSource: MatTableDataSource<MedicionGlucosa>;
@ViewChild(MatPaginator, { static: true }) paginator: MatPaginator;
@ViewChild(MatSort, { static: true }) sort: MatSort;
constructor() {
this.dataSource = new MatTableDataSource(this.registros);
this.dataSource.paginator = this.paginator;
this.dataSource.sort = this.sort;
}
ngOnInit() {
}
applyFilter(filterValue: string) {
this.dataSource.filter = filterValue.trim().toLowerCase();
if (this.dataSource.paginator) {
this.dataSource.paginator.firstPage();
}
}
}
Ahora el componente “tabla-registros.component” se muestra correctamente, pero la tabla aparece vacía, aunque tengamos registros capturados. Esto es porque, aunque la tabla tiene todos los elementos para almacenar y mostrar datos, no tenemos manera de comunicarnos con el componente para enviar o recibir datos.
Paso 3: Pasar datos al componente utilizando @Input()
Para pasar datos de un componente a otro, en este caso, desde el componente “registro” hacia “tabla-registros” necesitamos agregar una propiedad al componente con el decorador @Input().
Modifica la propiedad “registros” e inserta el siguiente código en tabla-registros.component.ts:
// registros = [] as MedicionGlucosa[];
_registros = [] as MedicionGlucosa[];
@Input()
set registros(value: MedicionGlucosa[]) {
this._registros = value;
this.dataSource.data = this._registros;
}
Lo primero que hacemos es cambiar el nombre del arreglo de mediciones en la tabla de “registros” a “_registros”. Esto es porque estamos agregando una nueva propiedad con el nombre “registros”. El decorador @Input() indica que el valor de esta propiedad puede asignarse desde otro componente. La manera más sencilla de utilizar @Input() es la siguiente:
@Input() registros: MedicionGlucosa[] = [];
Sin embargo, en este caso necesitamos ejecutar algunas operaciones cuando el valor de la propiedad “registros” cambia:
- Primero, tomamos el valor recibido “value” y lo guardamos en el arreglo “_registros”.
- Actualiza la fuente de datos de la tabla de angular material (mat-table).
Ahora que tenemos una forma de recibir datos, agrega el siguiente atributo al componente “tabla-registros” en "registro.component.html":
De esta manera, estamos indicando que el contenido de la propiedad “registros” del componente “registro.component” va a insertarse a la propiedad de entrada “[registros]” del componente “tabla-registros”.
Ahora podemos observar que la tabla que ahora existe dentro del componente “tabla-registros” está mostrando los datos que recibe desde el componente “registro”.
Paso 4: Escuchar eventos de un componente utilizando EventEmmiter<T>
Primero estamos creando una propiedad de tipo EventEmmiter<string> y decorándola con “@Output()”. Después, en la función “Eliminar()” estamos recibiendo como parámetro el Id del registro a eliminar y emitiendo el evento “itemDeleted” con el Id recibido.
export class TablaRegistrosComponent implements OnInit {
...
@Output() itemDeleted = new EventEmitter<string>();
...
Eliminar(id: string) {
this.itemDeleted.emit(id);
}
}
Anteriormente, el código para eliminar un registro estaba dentro de la función Eliminar, sin embargo, el componente “tabla-registros” tiene como única responsabilidad mostrar datos en la pantalla y notificar a otros componentes acerca de las interacciones del usuario, por ejemplo, cuando el usuario hace click en el botón eliminar.
@Output()
El decorador “@Output()” indica que podemos leer el valor de la propiedad desde otro componente, de una manera muy similar a como lo hacemos con el decorador “@Input()”.
EventEmmiter<T>
EventEmmiter es una clase incluida en Angular que se usa en propiedades con el decorador “@Output()” para emitir eventos personalizados a los que podemos suscribirnos. “<T>” indica que al crear un EventEmmiter podemos configurar el tipo de dato que va a incluir el evento. En este caso estamos configurando el evento para contener un string.
Ahora que tenemos listo el EventEmmiter, podemos suscribirnos al evento itemDeleted para ejecutar alguna función cuando sea invocado. Agrega el siguiente atributo a “app-tabla-registros” en registro.component.html:
De esta manera, el componente “registro.component” queda suscrito al evento, y cuando el usuario haga click en el botón eliminar, el componente “tabla-registros” dispara el evento “itemDeleted” y entonces el componente “registro.component” ejecuta la función “Eliminar()”.
- Cuando el usuario hace click en el botón para eliminar un registro, el componente “tabla-registros” ejecuta la función “Eliminar()”.
- La función “Eliminar()” en “tabla-registros.component.ts” emite el evento “itemDeleted” que contiene el Id del registro a eliminar.
- El componente “registro.component” está suscrito al evento, y cuando el evento se emite, ejecuta la función “Eliminar()” pasando como parámetro el Id del registro a eliminar.
- La función “Eliminar()” en “registro.component.ts” se encarga de recibir el Id del registro a eliminar y llama al servicio encargado del almacenamiento de datos para que sea eliminado.
Paso 5: Reutilizar el componente “tabla-registros”
Ahora que tenemos un componente independiente y reutilizable, podemos insertar la tabla de registros en la página principal de la aplicación. Agrega el componente al dashboard con el siguiente código en dashboard.component.html:
...
<mat-card>
<app-tabla-registros></app-tabla-registros>
</mat-card>
La tabla se muestra correctamente, pero de nuevo nos encontramos con que no tiene datos para mostrar. Esto es porque el componente “tabla-registros” necesita recibir los datos a través del atributo de entrada “[registros]”.
Ahora debemos actualizar “dashboard.component.ts” para que recupere los datos y los pase a la tabla:
...
export class DashboardComponent implements OnInit {
registros = [] as MedicionGlucosa[];
constructor(private mediciones: MedicionesService) {
}
ngOnInit() {
this.registros = this.mediciones.GetAll();
}
}
Ahora el componente tiene una propiedad “registros” que contiene los registros capturados por el usuario.
Utilizando Dependency Injection de Angular, en el constructor se está inyectando el servicio “MedicionesService” que se encarga de leer y escribir los registros de mediciones. De esta manera, Angular se encarga de que una instancia del servicio esté disponible en el componente.
Después, en la función “ngOnInit()” indicamos que una vez que el componente se cargue, recupere los registros de mediciones y los guarde en la propiedad “registros”.
Por último, necesitamos pasar los registros al componente “tabla-registros” utilizando el atributo de entrada “[registros]”. Actualiza “dashboard.component.html”:
Ahora la tabla de registros está disponible también en la página principal reutilizando el componente “tabla-registros” sin la necesidad de tener código duplicado. De esta manera la aplicación es más fácil de actualizar y mantener.
Nuevo problema
Ahora tenemos un nuevo problema. Supongamos que, por seguridad, el usuario debe poder eliminar capturas de mediciones desde la página de registro, pero no desde la página principal de la aplicación.
Necesitamos actualizar el componente “tabla-registros” para que podamos configurar si los botones para eliminar registros están disponibles en ciertos casos.
Para esto, necesitamos hacer algunas modificaciones en “tabla-registros.component.ts”:
...
export class TablaRegistrosComponent implements OnInit {
…
@Input() permitirEliminar = true;
…
// displayedColumns: string[] = ['fecha', 'nivel', 'comida', 'antesDespues', 'actions'];
…
GetDisplayerColumns() {
const displayedColumns: string[] = ['fecha', 'nivel', 'comida', 'antesDespues'];
if (this.permitirEliminar) {
displayedColumns.push('actions');
}
return displayedColumns;
}
}
Estas modificaciones consisten en lo siguiente:
- Agregamos una nueva propiedad de entrada “permitirEliminar”, con un valor verdadero (“true”) por default.
- Eliminamos la propiedad “displayedColumns” que guardaba un arreglo con los nombres de las columnas que se muestran en la tabla.
- Creamos una función nueva “GetDisplayedColumns()” que tiene como valor de retorno un arreglo con los nombres de las columnas que se mostrarán en la tabla. Esta función evalúa el valor de la propiedad “permitirEliminar” y si es verdadero entonces agrega la columna “actions” que es la que contiene el botón para eliminar registros.
Ahora necesitamos actualizar “tabla-registros.component.html” para que lea la lista de columnas desde la función “GetDisplayedColumns()” que acabamos de crear:
De esta manera, el componente “tabla-registros” muestra los botones para eliminar por default, pero ahora podemos configurarlo para ocultarlos si es necesario.
Para ocultar los botones en la pantalla principal de la aplicación, agrega el atributo de entrada “permitirEliminar” en “dashboard.component.html”:
De esta manera, evitamos que la tabla muestre los botones para eliminar registros en el dashboard de la aplicación.
Conclusión
En este post continuamos construyendo GLTrack, una aplicación para registro de mediciones de glucosa en la sangre para pacientes de diabetes.
Para evitar tener código duplicado en la aplicación, creamos un componente reutilizable para mostrar la tabla de registros en la página principal y en la página de registros de la aplicación.
Utilizamos los decoradores “@Input()” y “@Output()” así como EventEmmiter<T> para interactuar entre componentes de angular y pasar datos de entrada y salida.
También reutilizamos servicios de angular en diferentes componentes con Dependency Injection de Angular 8.
En el siguiente post, continuaremos trabajando en el Dashboard de la aplicación, para mostrar datos relevantes para el usuario acerca de sus mediciones de glucosa a través de graficas que muestren los datos capturados en la aplicación de manera visual.
El código fuente completo de la aplicación de ejemplo puedes encontrarlo en GitHub, en https://github.com/RCKSTR1/GLTrack3 o si prefieres, puedes clonar el repositorio directo en https://github.com/RCKSTR1/GLTrack3.git
Si tuviste algún problema o duda al seguir los pasos, o si tienes alguna pregunta acerca de este post, contáctame en Twitter, Facebook o LinkedIn.
Es cuánto.
Fuentes
- Documentación de Angular. "Propiedades de @Input() y @Output()". https://angular.io/guide/template-syntax#input-and-output-properties
- Documentación de Angular. "EventEmitter". https://angular.io/api/core/EventEmitter#eventemitter