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:

  1. Eliminar el atributo href de la etiqueta del enlace.
  2. Que la función deleteClient retorne un valor lógico boolean como false. 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>&nbsp;

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>&nbsp;

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>&nbsp;</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.

Enlaces externos