domingo, 8 de abril de 2018

ngx-model: Libraría para la gestión del estado en Angular

A la hora de pensar como gestionar el estado en nuestras aplicaciones Angular lo lógico es pensar en utilizar una librería como ngrx.

El problema de ngrx es que supone un cambio importante en el paradigma de programación tal como estamos acostumbrados. Sin embargo, resulta indudable que tener separado el estado de la aplicación como una única fuente de verdad absoluta a la que todos consultan y donde todos modifican tiene grandes ventajas.

Si queremos gestionar el estado de nuestras aplicaciones Angular sin tener que recurrir a la complejidad de la librería ngrx, podemos usar Angular Model Pattern, una librería en la que en palabras de su autor sirve para Gestión de estados sencilla con una API minimalista, flujo de datos unidireccional, compatibilidad con múltiples modelos y datos inmutables expuestos como RxJS Observable

Para usar esta librería tenemos dos opciones:

1. instalando la dependencia ngx-model: npm install ngx-model --save
2. Creando nosotros el servicio tal como se indica en la página de la librería, en el apartado  "Use as Pattern" (https://tomastrajan.github.io/angular-model-pattern-example#/getting-started)

En cualquiera de los dos casos lo que la librería hace es, una vez que especificamos los tipo de datos con el que vamos a trabajar, -haciendo uso de tipos genéricos- usar un tipo "especial" de Observable (BehaviorSubject), para proporcionar métodos para la consulta y modificación de los datos del modelo, y otros métodos como el clonado de objetos manteniendo la inmutabilidad de los mismos.

Para hacer uso de la lbrería tenemos que:

- Importar ModelFactory en nuestro servicio, inyectándolo como dependencia en el constructor y crear una instancia de modelo.


      // inject model factory with specified type
      constructor(private modelFactory: ModelFactory<Todo[]>)
 

En los componentes o servicios que se quiera consultar/modificar el estado tendriamos que inyectar el servicio que hemos creado y hacer uso de los métodos del mismo que son los que gestionan el estado de nuestro modelo.

Vamos a verlo en un ejemplo que nos va a permitir guardar, actualizar y eliminar datos como nombre, apellidos y fecha de nacimiento de nuestros contactos que queramos almacenar.

Como queremos gestionar el estado de nuestros contactos vamos a crear una interfaz que represente este modelo.

export interface Contacto {
id: string;
nombre: string;
apellidos: string;
fechaNacimiento: string;
}

Vamos a crear un servicio de modelo donde utilizar esta interfaz para instanciar el modelo de nuestras contactos e implementar los métodos que cambian y consultan los datos del modelo, es decir, métodos para inserción, actualizacióny borrado de elementos.

El código de nuestro servicio es el siguiente:

@Injectable()
export class ContactoService {
private model: Model<Contacto[]>;
contacto$: Observable<Contacto[]>;

constructor(private modelFactory: ModelFactory<Contacto[]>) {
// create model and pass initial data
this.model = this.modelFactory.create([]); 
// expose model data as named public property
this.contacto$ = this.model.data$;
}

.........
}


En nuestro servicio hemos creado el modelo como un array de contactos a través del modelFactory y asignado a un Observable del mismo tipo.

El servicio tiene dos metodos. Uno para crear "contactos" que lo que hace es a través de la función set de la clase establecer el valor del "nuevo" modelo, que será lo que ya teniamos concatenando el nuevo contacto:

addContacto(nombre: string, apellidos: string,  
fechaNacimiento: Date) {
const id = generateUid('-');
const contactos = this.model.get();
this.model.set(contactos.concat([{ id, nombre, apellidos,  
fechaNacimiento }]));
}

y otro método para eliminar un contacto existente por medio de su Id, (que habremos generado a traves del método generateUid tal como puede observarse en el código anterior)

removeContacto(id) {
const contactos = this.model.get();
const index = contactos.findIndex(x => x.id === id);
if (index !== -1) {
this.model.set([...contactos.slice(0, index), 
 ...contactos.slice(index + 1, contactos.length)]);
}
}

Para el método de eliminar, obtenemos los contactos usando el método get() del modelo (model.get()) para posteriormente buscar el indice del elemento cuyo id es el especificado.
Si lo encontramos, establecemos el nuevo modelo dejando "fuera" el elemento encontrado, es decir, eliminando en otras palabras. Esto lo hacemos usando la propedad de inmutabilidad por medio del spread operator de ES6 para no tener que modificar el array inicial.

Por otra parte vamos a crear tres componentes, uno para crear nuestros contactos, otro para listarlos y un tercero que será el detalle del ccontacto en sí mismo.

El componente que crea los contactos (ContactoNuevoComponent) tendrá el siguiente layout:

<h1 class="title">
Añadir un contacto</h1>
<div class="field">
<label class="label">Nombre</label>
<div class="control">
<input class="input" type="text" placeholder="Nombre" #nombre>
</div>
</div>
<div class="field">
<label class="label">Apellidos</label>
<div class="control">
<input class="input" type="text" placeholder="Apellidos" 
#apellidos>
</div>
</div>
<div class="field">
<label class="label">Fecha nacimiento</label>
<div class="control">
<input class="input" type="text" 
 placeholder="Fecha nacimiento" #fechanacimiento>
</div>
</div>
<div class="field is-grouped">
<div class="control">
<button class="button is-link" 
(click)="addContacto(nombre.value, apellidos.value, 
fechanacimiento.value)">Añadir contacto</button>
</div>
</div>

en cuanto al código, unicamente tendremos el método addContacto que recibe como parámetros el nombre, apellidos y fecha de nacimiento del contacto que queremos crear:

addContacto(nombre: string, apellidos: string
 fechaNacimiento: Date): void {
this._contacto.addContacto(nombre, apellidos, fechaNacimiento);
}

En este método llamamos al método addContacto del servicio _contacto (ContactoService) que tendremos que inyectar en el constructor

El componente que nos va a mostrar nuestros contactos (ContactoListComponent) no tiene mucha historia. Vamos a cargar los contactos de nuestro modelo haciendo uso del servicio de contactos y a pasar el propio contacto al componente detalle que recibe como parametro Input() el contacto.

<div class="card" *ngFor="let contacto of _contacto.contacto$ | async;">
<app-contacto-detail [contacto]="contacto"></app-contacto-detail>
</div>


el componente que muestra el detalle (ContactoDetailComponent) recibe como parametro Input() el propio contacto y lo muestra en el template:
export class ContactoDetailComponent implements OnInit {
@Input() contacto: Contacto;
constructor(public _contacto: ContactoService) { }

<div class="card-content">
<div class="content">
<p class="card-header-title">
<span class="title">{{contacto.nombre}} {{contacto.apellidos}}</span>
</p>
<span class="subtitle">{{contacto.fechaNacimiento}}</span>
</div>
</div>
<footer class="card-footer">
<a href="#" class="card-footer-item" (click)="eliminarContacto(contacto.id)">Eliminar</a>
</footer>


y el método para eliminar el contacto llamando al método removeContacto del servicio:

/**
* Eliminar un contacto
* @param id
*/
eliminarContacto(id) {
this._contacto.removeContacto(id);
}


El resultado se verá similar al de la siguiente imagen:



En esta entrada hemos visto como podemos gestionar el estado en angular haciendo uso de la librería ngx-model, que si bien no tiene toda la potencia que nos ofrece las librerías de redux para angular, tampoco tiene la misma complejidad y puede sernos útil en multiples situaciones.

El código fuente se encuentra disponible en https://github.com/jorgetrigueros/ngx-model










No hay comentarios:

Publicar un comentario