Aplicación para gestionar información sobre clientes: Nombre y NIF. Debe ofrecer la posibilidad de editar un cliente, borrarlo o añadir uno nuevo.
Creamos un nuevo proyecto:
ng new angular2-clientes
El proyecto se debe generar con la opción de rutas, la URL del SPA apunta a diferentes rutas que cargan una vista de un componente u otro, más abajo se explica en que consisten las rutas.
Modelo de datos
Normalmente cualquier proyecto comienza definiendo el modelo de datos del que se alimenta la APP.
[src/app/modelo/cliente.ts]
export class Cliente {
id: number;
nombre: string;
nif: string;
}
La declaración de la clase Cliente
está precedida por la palabra clave export
para poder ser instanciada en otros archivos de nuestro proyecto.
Mocks: Definición de datos de prueba
En la POO se llama mocks a los objetos simulados que imitan el comportamiento de los reales, para comenzar a probar nuestra APP es la mejor aproximación antes de interactuar con un servidor REST HTTP más adelante en este mismo tutorial.
Contenido de [src/app/mocks/mock-clientes.ts]:
import { Cliente } from "../modelo/cliente";
export const CLIENTES: Cliente[] = [
{ id: 1, nombre: "Javier", nif: "12345678Z" },
{ id: 2, nombre: "Pepe", nif: "Y2345678Z" },
{ id: 3, nombre: "Pedro", nif: "87654321A" }
];
La primera línea importa la definición de clase para poder instanciar los objetos del array CLIENTES
.
Igual que en el modelo la definición de la variable va precedida de la palabra clave export
para poder referirnos a la variable más adelante.
La palabra reservada const
evita que podemos reasignar la variable. Eso no quiere decir que los valores que contiene sean inmutables (podemos acceder a los elementos del arreglo y modificar un atributo de un objeto sin problema). Es una buena práctica habitual es muchos lenguajes definir las constantes en mayúsculas.
La variable contiene un array de objetos. Existen varias formas de declarar un array, en este caso primero definimos el tipo de elementos que contendrá seguido de los corchetes [].
Para saber más sobre arrays este enlace de tutorialspoint está muy bien.
Cada elemento del array contiene un objeto.
Rutas
En las páginas Web tradicionales (como las que hacía yo en PHP) introducimos la dirección URL, cuando hacemos click sobre nuevos enlaces nos redirige a nuevas páginas. Angular rompe con esta concepción la idea general de una SPA es tener una única página que cargue de forma dinámica otras vistas. Normalmente la página contenedora (app.component.html) mantiene el menú de navegación, el pie de página y otras áreas comunes. Y deja un espacio para la carga dinámica.
Para saber más sobre rutas recomiendo la Web oficial de Angular donde habla de "Routing & Navigation".
[src/index.html] contiene una etiqueta <base>
en la cabecera <head>
que le dice al router como debe componer las URLs. Si la carpeta app es la raíz de nuestra APP lo dejamos como está:
<base href="/">
El Router de Angular es opcional y no es parte del core, tiene su propia librería @angular/router
.
[src/app/app-routing.module.ts]
import { Routes, RouterModule } from "@angular/router";
Dentro definimos un array routes
compuesto de objetos tipo Route
donde definimos las dirección que resuelve (path
) y el componente que debe cargar.
const routes: Routes = [
{ path: "clientes/listado", component: ClientesListadoComponent },
{ path: "clientes/detalles", component: ClientesDetallesComponent }
];
En [src/app/app.component.html] se añade una etiqueta <router-outlet></router-outlet>
que maneja las rutas en el componente [src/app/app.routing.module.ts]. Los enlaces que nos llevan a cada página se definen con un atributo routerLink
que indica la ruta.
[src/app/app.component.html]: Vista con atributo routerLink
con ruta en el enlace.
<a routerLink="clientes/listado">Listado Clientes</a>
Servicios
Un Servicio en Angular es el mecanismo para compartir funcionalidad entre componentes (por ejemplo una conexión de datos). Los componentes no deberían trabajador contra el modelo de datos directamente, en su lugar creamos un servicio que accede al mock con los datos de prueba (más adelante crearemos un servidor REST JSON para simular las llamadas al servidor).
Los servicios son clases TypeScript.
ng generate service clientes
[src/app/servicios/client.service.ts]
import { Injectable } from "@angular/core";
import { Observable, of } from "rxjs";
import { Cliente } from "../modelo/cliente";
import { CLIENTES } from "../mocks/mock-clientes";
@Injectable({
providedIn: "root"
})
export class ClientesService {
getClientes(): Observable<Cliente[]> {
return of(CLIENTES);
}
}
El decorador @Injectable
(viene de @angular/core
) e indica que esta clase puede ser inyectada de forma dinámica a quien la demande (en ClientesListadoComponent
por ejemplo).
Angular funciona a través de inyección de dependencias lo cual significa que puedes pasar una referencia a una instancia en diferentes componentes y te permitirá utilizarla en diferentes partes de tu App.
Dentro de la clase ClientesService
creamos un método para obtener la lista de clientes del mock:
getClientes(): Observable<Cliente[]> {
return of(CLIENTES);
}
La función retorna un array de clientes Observable, este tipo ayuda a manejar datos de forma asíncrona. Desde [src/app/componentes/clientes-listado/clientes-listado.component.ts] modificamos la función ngOnInit()
(se ejecuta cada vez que se carga el componente) para que se suscriba al Observable.
ngOnInit() {
this.clientesService
.getClientes()
.subscribe(
(clientesRecibidos: Cliente[]) => (this.clientes = clientesRecibidos)
);
}
El método subscribe
del Observable
es usado por los componentes de Angular para suscribirse a los mensajes que son enviados a el.
Dentro de subscribe
declaramos una función lambda (o función fecha), es una forma reducida de expresar una función (introducia en ES6), la sintaxis básica es la siguiente:
Identifier => Expression;
Evita que usemos las palabras function
o return
por ejemplo. En la función de arriba recibe como parámetro un arreglo de clientes clientesRecibidos
y en la declaración lo copia a un atibuto interno this.clientes
de la clase ClientesListadoComponent
.
Borrado de un cliente
Si ahora pinchamos sobre un enlace para borrar un usuario por ejemplo no está la función programada aún pero recarga toda la página de nuevo. En las Webs clásicas los navegadores cuando se usaba un enlace pedían la ruta al servidor HTTP, este lo buscaba y le devolvía el documento HTML para que lo renderizase el navegador (lazy loading o carga diferida). En las aplicaciones SPA es el cliente el responsable de cargar el contenido asociado a cada ruta.
Ahora mismo los enlaces para borrar o editar un registro de la tabla no tienen una función asociada para manejador el evento cuando pinchamos sobre ellos. Modificamos [src/app/componentes/clientes-listado/clientes-listado.component.html].
Empezamos definiendo la función asociada a la operación de borrado de un cliente ya que es la más sencilla, modificamos el enlace:
<a href="#" (click)="deleteClient(cliente)">Borrar</a>
Para evitar que recargue la página cuando pinchamos sobre el enlace tenemos varias opciones:
- Eliminar el atributo
href
de la etiqueta del enlace. - Que la función
deleteClient
retorne un valor lógicoboolean
comofalse
. De esta forma los controles padre no reciben este evento.
Detalles de clientes
Para modificar los datos de un cliente pinchando en la lista de clientes antes de nada debemos resolver como enviar el ID del cliente de un componente a otro usando las Routes de Angular, por ejemplo si queremos editar un cliente con un valor de ID igual a 1 la ruta será "clientes/detalles/1".
Declarando los parámetros de la ruta: En [src/app/app-routing.module.ts] debemos modificar el array con los objetos de las rutas que declaramos. El ":" indica que eso no es una cadena estática, sino un parámetro.
{ path: "clientes/detalles/:id", component: ClientesDetallesComponent }
Podemos declarar tantos parámetros como queramos, por ejemplo "clientes/detalles/:nombre/:apellidos" que podría traducir como "clientes/detalles/iker/landajuela" por ejemplo. La única limitación es que el número de parámetros definidos debe ser el mismo, ni más, ni menos.
Ahora debemos asociar la ruta al parámetro, modificamos [src/app/componentes/clientes-listado/clientes-listado.component.html].
Para probar definimos el enlace así:
<a [routerLink]="['/clientes', 'detalles', '1']">Editar</a>
Pasamos un array como valor a la directiva routerLink
. No tiene mucho sentido dejarlo así ya que queremos que sean variables dinámicas, volemos a modificar el enlace:
<a [routerLink]="['/clientes', 'detalles', cliente.id]">Editar</a>
Y está última forma es otra alternativa que hace lo mismo:
<a routerLink="/clientes/detalles/{{ cliente.id }}">Editar</a> & nbsp;
Ahora toca leer los parámetros enviados en [src/app/componentes/clientes-detalles/clientes-detalles.component.ts], esta parte es un poco más complicada. En la cabecera del fichero hemos añadido la importación de una nueva clase ActivatedRoute
del modulo @angular/router
.
import { Router, ActivatedRoute } from "@angular/router";
En el constructor de la clase ClientesDetallesComponent
hemos inyectado un nuevo argumento ruta
de la clase ActivatedRoute
.
constructor(
private clientesService: ClientesService,
private router: Router,
private ruta: ActivatedRoute
) {}
El método ngOnInit()
que se ejecuta cuando se carga el componente trabaja con la variable ruta
ngOnInit(): void {
const id: number = +this.ruta.snapshot.paramMap.get("id");
console.log("ngOnInit. id:" + id);
if (id) {
this.clientesService.getCliente(id).subscribe(cliente => {
this.cliente = cliente;
console.log(this.cliente);
});
}
}
El snapshot te da los parámetros del componente en el instante que los consultes.
A continuación con el ID del cliente debemos obtener su información llamando a una nueva función observable del servicio. La función recibe como argumento el ID y lo busca usando el método find
getCliente(id: number): Observable<Cliente> {
return of(this.clientes.find(cliente => cliente.id === id));
}
La función recibe como argumento el ID y lo busca usando el método find
, este método recibe como parámetro una función de prueba que en este caso es una expresión lambda que compara el ID recibido con el ID de los elementos del array.
Sintaxis:
arr.find(callback[, thisArg])
callback es la función que se ejecuta para cada elemento del array.
Guardando las modificaciones
Plantilla del componente:
<input #id type="hidden" id="id" value="{{ cliente.id }}" />
<div>
<div>
<label for="nombre">Nombre</label>
<input #nombre type="text" id="nombre" value="{{ cliente.nombre }}" />
</div>
<div>
<label for="nif">NIF</label>
<input #nif type="text" id="nif" value="{{ cliente.nif }}" />
</div>
<div>
<label> </label>
<button (click)="guardar(+id.value, nombre.value, nif.value)">
Aceptar
</button>
</div>
</div>
A continuación debemos definir el método guardar
en "clientes-detalles.components.ts" para guardar los cambios del formulario, la función putCliente
está definida en el servicio.
guardar(id: number, nombre: string, nif: string): void {
let cliente: Cliente = { id: id, nombre: nombre, nif: nif };
console.log("EMITIDO", cliente);
this.clientesService.putCliente(cliente).subscribe(cliente => {
console.log("RECIBIDO", cliente);
this.router.navigate(["/clientes/listado"]);
});
}
Nos suscribimos a la función putCliente
que es observable y retorna un objeto cliente
de forma asíncrona (una función observable provee de un mecanismo para intercambiar mensajes entre la función suscriptora o consumidora guardar
y la que publica que es putCliente
). El objeto cliente
es notificado por la función observable cuando acaba y ejecuta una función fat arrow para mostrar de nuevo la tabla de clientes (usando las rutas de Angular).
[src/app/componentes/servicios/clientes.service.ts]
putCliente(cliente: Cliente): Observable<Cliente> {
const posicionArray = this.clientes.findIndex(
clienteRepositorio => clienteRepositorio.id === cliente.id
);
this.clientes[posicionArray] = cliente;
return of(cliente);
}
El método putCliente
del servicio recibe como argumento un objeto Cliente
, lo primero es buscar cual es dentro del array usando el método findIndex
. Esta función devuelve el índice del primer elemento de un array que cumple con la función de prueba proporcionada como argumento, en caso contrario devuelve -1. Como argumento hemos proporcionado una función flecha (lambda o fat arrow por e símbolo "=" de la flecha) que se ejecuta sobre cada uno de los elementos del array.
Añadir un nuevo cliente
Código fuente del proyecto
El proyecto original está desarrollado por Javier Lete y está alojado en GitHub en este enlace.
- angular / src / 01-intro / angular2-clientes.
Enlaces externos
- "Páginas y rutas Angular SPA | Academia Binaria".
- "Angular - Introduction to services and dependency injection".
- "Angular: Como crear un Servicio. - Ng-Classroom".
- "Angular 4 Services - Tutorialspoint".
- "Angular Observable Data Services - Angular 7 - Cory Rylan".
- "How to remove an item from an Array in JavaScript - Flavio Copes".
- hacks.mozilla.org "ES6 In Depth: Arrow functions".
- "Parámetros en las rutas Angular - DesarrolloWeb".
- "Array.prototype.find() - JavaScript | MDN".