Angular & NgRx Data

Hernan Castilla
7 min readJul 6, 2021

--

Ngrx/data

Ngrx data es una extensión de ngrx que nos permite minimizar la cantidad de información o complejidad de nuestro modelo de datos, podemos llamarlo una automatización de todo nuestro flujo de trabajo en nrgx, https://ngrx.io/guide/data.

El flujo normal se ve de esta manera

Nosotros vamos agregando capas a medida que necesitemos, tenemos control sobre todo el proceso lo cual puede resultar extenso y más complejo.

En ngrx data tendríamos un flujo parecido, pero sería una caja negra para nosotros, todo ese flujo lo gestionaría directamente ngrx data

https://slides.com/jiali/deck-5

Nuestros componentes interactuarían de forma directa solo con el entityCollectionService, dándonos acceso a las operaciones crud de nuestras entidades y por debajo este desencadenaría todo lo necesario para seguir con su flujo.

Cabe mencionar que en el diagrama vemos la capa de DataService, está una de las curiosidades debido a que el mismo ngrx tiene la capacidad de hacer las llamadas a nuestro backend de forma automática, no necesitaríamos programar estos servicios de conexión a la api, en caso de ser necesario también podemos crear dataService personalizados para gestionar el acceso a la data, imaginemos que es en memoria, podríamos crear un dataService que en vez de ir a un backend devuelva la data en memoria para el flujo de ngrx.

Manos a la obra

No podemos desligar ngrx de angular, por lo que el primero paso es crear el proyecto con https://cli.angular.io/

Creamos el proyecto

ng new ngrx-data
cd ngrx-data

con el proyecto en angular listo procedemos a instalar ngrx data, debemos tener en cuenta que para esto debemos instalar la suit completa, debido a que por dentro crea un flujo completo pasando por efectos y entidades.

ng add @ngrx/store@latest
ng add @ngrx/effects@latest
ng add @ngrx/entity@latest
ng add @ngrx/data@latest
ng add @ngrx/store-devtools@latest

Si todo se instala de forma correcta, en nuestro app.module.ts debemos tener algo parecido excluyendo HttpClientModule, deben importarlo debido a ngrx data se encarga de hacer llamadas a la api y dará error si no encuentra el módulo.

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { EntityDataModule } from '@ngrx/data';
import { entityConfig } from './entity-metadata';
import { EffectsModule } from '@ngrx/effects';
import { StoreModule } from '@ngrx/store';
import { HttpClientModule } from '@angular/common/http';
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
import { environment } from '../environments/environment';
@NgModule({
declarations: [AppComponent],
imports: [
BrowserModule,
AppRoutingModule,
StoreModule.forRoot({}),
EffectsModule.forRoot([]),
EntityDataModule.forRoot(entityConfig),
StoreDevtoolsModule.instrument({
maxAge: 25,
logOnly: environment.production,
}),
HttpClientModule,
],
providers: [],
bootstrap: [AppComponent],
})
export class AppModule {}

Vamos a crear una pequeña libreta de contacto, lo primero es definir nuestra entidad, es la base de todo.

/entities/contact.ts

export interface Contact {
readonly id: number;
readonly name: string;
readonly number: number;
}

Con nuestra entidad definida, debemos registrarla en nuestro entity-metadata que después se encargara de hacer el match entre nuestro entityCollectionService y la entidad correspondiente

/entity-metadata.ts

import { EntityMetadataMap, EntityDataModuleConfig } from '@ngrx/data';const entityMetadata: EntityMetadataMap = {
Contact: {},
};
const pluralNames = {
Contact: 'Contacts',
};
export const entityConfig: EntityDataModuleConfig = {
entityMetadata,
pluralNames,
};

Con esto ya estamos listo para ir a nuestro componente, en este caso voy a realizar toda la lógica en el app.component.ts, el primero paso es obtener un entityCollectionService, esto realmente se puede hacer de dos maneras, utilizando un factory directamente en el componente o creando una clase aparte y extendiéndola de EntityCollectionServiceBase

Vamos poco a poco, creamos una variable, contactCollectionService, implementamos un constructor de nuestro app.component.ts e inyectamos el entityCollectionServiceFactory para crear nuestro entityCollectionService.

import { Component, OnInit } from '@angular/core';
import {
EntityCollectionServiceFactory,
} from '@ngrx/data';
import { Contact } from './entities/contact';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
})
export class AppComponent {
title = 'ngrx-data';
contactCollectionService;
constructor(
private entityCollectionServiceFactory: EntityCollectionServiceFactory
) {
this.contactCollectionService =
this.entityCollectionServiceFactory.create<Contact>('Contact');
}
}

Listo, tenemos nuestra colección de contactos, vamos a implementar unos cuantos métodos para obtener los contactos y créalos.

Aquí podemos observar todos los métodos que nos brinda el entityCollectionService https://ngrx.io/api/data/EntityCollectionService

Intentemos obtener los contactos, vamos a implementar el OnInit y una función getAll

import { Component, OnInit } from '@angular/core';
import { EntityCollectionServiceFactory } from '@ngrx/data';
import { Contact } from './entities/contact';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
})
export class AppComponent implements OnInit {
title = 'ngrx-data';
contactCollectionService;
constructor(
private entityCollectionServiceFactory: EntityCollectionServiceFactory
) {
this.contactCollectionService =
this.entityCollectionServiceFactory.create<Contact>('Contact');
}
ngOnInit(): void {
this.getAll();
}
getAll() {
this.contactCollectionService.getAll();
}
}

Si vamos al navegador y vemos la consola podemos observar un error

GET <http://localhost:4200/api/contacts/> 404 (Not Found)

Excelente, como lo mencionamos la capa de DataService implementa de forma automática lo necesario para hacer las llamadas correspondientes a la api, como podemos observa el path corresponde a la configuración en plural que definimos en el entity-metadata

Vamos a levantar un pequeño servidor con json-server para hacer las pruebas

npm install json-server --sD

Creamos un script en nuestro package.json

"json-server": "json-server --watch db.json"

Y creamos un archivo db.json en la raíz del proyecto

{
"contacts":[]
}

Listo, tenemos nuestro servidor corriendo en el puerto 3000, vamos a añadir unos cuantos contactos y validar que funcione

{
"contacts":[
{
"id":2,
"name": "contact 1",
"number": 3333333
},
{
"id":1,
"name": "contact 2",
"number": 222222
}
]
}

Procedemos a cambiar la configuración de la url del collectionService, definimos un defaultDataServiceConfig en el app.module.ts y creamos un provider

...const defaultDataServiceConfig: DefaultDataServiceConfig = {
root: '<http://localhost:3000>',
timeout: 3000, // request timeout
};
@NgModule({
...
providers: [
{ provide: DefaultDataServiceConfig, useValue: defaultDataServiceConfig },
],
...
})
export class AppModule {}

Vamos de nuevo al navegador y validamos que en el network que tengamos como respuesta lo siguiente

// <http://localhost:3000/contacts/>
[
{
"id": 1,
"name": "contact 1",
"number": 3333333
},
{
"id": 2,
"name": "contact 2",
"number": 222222
}
]

Ahora vamos a renderizar esto en el html, creamos una variable contacts de tipo observable y en el constructor le establecemos el valor de las entidades de nuestro contactCollectionService

export class AppComponent implements OnInit {
title = 'ngrx-data';
contactCollectionService;
contacts: Observable<Contact[]>;
constructor(
private entityCollectionServiceFactory: EntityCollectionServiceFactory
) {
this.contactCollectionService =
this.entityCollectionServiceFactory.create<Contact>('Contact');
this.contacts = this.contactCollectionService.entities$;
}
ngOnInit(): void {
this.getAll();
}
getAll() {
this.contactCollectionService.getAll();
}
}

app.component.html

<h2>Contactos</h2>
<ul>
<li *ngFor="let contact of contacts | async">
<div>
<strong>Nombre: </strong>
{{contact.name}} |
<strong>Numero: </strong>
{{contact.number}} |
<button>Eliminar </button>
</div>
<br>
</li>
</ul>

Vamos a eliminar un contacto, para esto debemos agregar una redirección de rutas a nuestro servidor debido a que los patchs son diferentes, creamos el archivo routes.json en la raíz del proyecto

{
"/contact": "/contacts",
"/contact/:id": "/contacts/:id"
}

Y modificamos el script de package.json

"json-server": "json-server --routes routes.json --watch db.json"

En el app.component.html modificamos el boton eliminar

<h2>Contactos</h2>
<ul>
<li *ngFor="let contact of contacts | async">
<div>
<strong>Nombre: </strong>
{{contact.name}} |
<strong>Numero: </strong>
{{contact.number}} |
<button (click)="delete(contact.id)">Eliminar </button>
</div>
<br>
</li>
</ul>

En el app.component.ts agregamos la función eliminar

import { Component, OnInit } from '@angular/core';
import { EntityCollectionServiceFactory } from '@ngrx/data';
import { Observable } from 'rxjs';
import { Contact } from './entities/contact';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
})
export class AppComponent implements OnInit {
... delete(id: number) {
this.contactCollectionService.delete(id);
}
}

Por último vamos a implementar un formulario para crear un contacto y damos por terminado el código, primero importamos el FormsModule y ReactiveFormsModule en app.module.ts

@NgModule({
declarations: [AppComponent],
imports: [
...
FormsModule,
ReactiveFormsModule
],
providers: [
{ provide: DefaultDataServiceConfig, useValue: defaultDataServiceConfig },
],
bootstrap: [AppComponent],
})
export class AppModule {}

Agregamos el formulario al html y un mensaje de loading

<h2>Crear Contacto</h2>
<form [formGroup]="form" (ngSubmit)="create()">
<input type="text" placeholder="Nombre" formControlName="name">
<input type="number" placeholder="Numero" formControlName="number">
<button type="submit" [disabled]="form.invalid"> Crear </button>
</form>
<h2>Contactos</h2><ng-container *ngIf="loading |async; else elseTemplate">
loading ...
</ng-container>
<ng-template #elseTemplate>
<ul>
<li *ngFor="let contact of contacts | async">
<div>
<strong>Nombre: </strong>
{{contact.name}} |
<strong>Numero: </strong>
{{contact.number}} |
<button (click)="delete(contact.id)">Eliminar </button>
</div>
<br>
</li>
</ul>
</ng-template>

Creamos el método crear en el app.component.ts

import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { EntityCollectionServiceFactory } from '@ngrx/data';
import { Observable } from 'rxjs';
import { Contact } from './entities/contact';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
})
export class AppComponent implements OnInit {
title = 'ngrx-data';
contactCollectionService;
contacts: Observable<Contact[]>;
form: FormGroup;
loading: Observable<Boolean>;
constructor(
private entityCollectionServiceFactory: EntityCollectionServiceFactory,
private fb: FormBuilder
) {
this.contactCollectionService =
this.entityCollectionServiceFactory.create<Contact>('Contact');
this.contacts = this.contactCollectionService.entities$; this.loading = this.contactCollectionService.loading$; this.form = this.fb.group({
name: ['', [Validators.required]],
number: ['', [Validators.required]],
});
}
ngOnInit(): void {
this.getAll();
}
getAll() {
this.contactCollectionService.getAll();
}
delete(id: number) {
this.contactCollectionService.delete(id);
}
create() {
const contact: Contact = this.form.value;
this.contactCollectionService.add(contact);
}
}

Listo, prácticamente tenemos nuestra libreta de contactos, antes de terminar vamos a separar nuestro contactCollectionService a una clase para poderlo inyectar donde lo necesitemos.

collectionServices/contact.collection.service.ts

import { Injectable } from '@angular/core';
import {
EntityCollectionServiceBase,
EntityCollectionServiceElementsFactory,
} from '@ngrx/data';
import { Contact } from '../entities/contact';
@Injectable({ providedIn: 'root' })
export class ContactCollectionService extends EntityCollectionServiceBase<Contact> {
constructor(serviceElementsFactory: EntityCollectionServiceElementsFactory) {
super('Contact', serviceElementsFactory);
}
}

Y el app.component.ts

import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Observable } from 'rxjs';
import { Contact } from './entities/contact';
import { ContactCollectionService } from './collectionServices/contact.collection.service';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
})
export class AppComponent implements OnInit {
title = 'ngrx-data';
contacts: Observable<Contact[]>;
form: FormGroup;
loading: Observable<Boolean>;
constructor(
private contactCollectionService: ContactCollectionService,
private fb: FormBuilder
) {
this.contacts = this.contactCollectionService.entities$;
this.loading = this.contactCollectionService.loading$; this.form = this.fb.group({
name: ['', [Validators.required]],
number: ['', [Validators.required]],
});
}
ngOnInit(): void {
this.getAll();
}
getAll() {
this.contactCollectionService.getAll();
}
delete(id: number) {
this.contactCollectionService.delete(id);
}
create() {
const contact: Contact = this.form.value;
this.contactCollectionService.add(contact, {
isOptimistic: false,
});
}
}

Listo, tenemos nuestro primer acercamiento a ngrx data, ahora solo queda observar todo el comportamiento del estado de la aplicación en el Redux DevTools, nos queda faltando una implementación custom del DataService y jugar un poco con el apartado del caché que también es algo muy interesante que nos proporciona ngrx data.

Código: https://github.com/hcastillaq/ngrx-data-example

Pero compartir no es inmoral –es un imperativo moral. Sólo aquellos que están cegados por la codicia se negarían a hacerle una copia a un amigo.

— Aaron Swartz

--

--

Hernan Castilla
Hernan Castilla

Written by Hernan Castilla

” Le vent se lève!… Il faut tenter de vivre!”

No responses yet