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.
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