En el artículo anterior dedicamos todo el contenido a preparar la estructura básica de la aplicación CRUD que queremos desarrollar. Ha llegado el momento de ponernos a trabajar «en serio».
En este artículo vamos a empezar desarrollando el componente que lista los miembros. Para ello, tendremos, no sólo que crear dicho componente, sino que también empezaremos a crear algunos contenidos complementarios: parte del código de uno de los servicios, la API que obtiene la lista, la estructura de la base de datos con algunos datos para probar… Aprenderemos como hacer una lista de datos en Angular, de un modo eficiente, aprovechando las prestaciones y funcionalidades que este framework nos ofrece.
Esto nos llevará a un pequeño esfuerzo para aprender y asimilar cosas nuevas. El resultado merecerá la pena.
Antes de seguir adelante, crea en tu localhost un directorio al que llamarás aca_apis
, donde iremos almacenando las API’s PHP para manejar el CRUD. Como este directorio correspondería a tu servidor, crea dentro un directorio llamado avatares
, donde se irán almacenando los avatares de los miembros. De todos modos, toda esta estructura, incluyendo una base de datos MySQL básica, te la pondré al final del artículo en un enlace, como siempre.
Vamos a trabajar.
LA BASE DE DATOS
Antes de ponernos a trabajar, vamos a conocer la base de datos en la que persistiremos los datos de los miembros. La he llamado ang_aca_miembros
y tiene una tabla llamada socios
, con la siguiente estructura:
CAMPO | USO | COMENTARIOS |
id |
Un identificador único para cada usuario | Un campo numérico autoincrementable, que actuará como clave primaria. |
doi |
El DNI, NIF, NIE, Pasaporte u otro documento | Un campo de cadena de longitud variable. Este campo tiene un índice de tipo UNIQUE para que no se puedan duplicar números de documento. |
nombre |
El nombre y apellido(s) | Un campo de cadena de longitud variable. |
fecha_de_ingreso |
La fecha en que ingresó en la aplicación | Un campo de fecha en el formato ISO 8601 extendido (el habitual de MySQL: YYYY-MM-DD ) |
genero |
El género (hombre o mujer) | Un campo de tipo enum, con los posibles valores H o M |
avatar |
La fotografía o avatar elegido por el usuario, si decide ponerlo. | Un identificador único generado por la API que hace la grabación. Los avatares estarán en formato JPG. |
activo |
El estado del usuario. | Un campo de tipo enum, para indicar si el miembro sigue activo o no. Los posibles valores son S o N . |
Los campos de tipo cadena de longitud variable se han establecido con la longitud convencional de 255 caracteres máximo. Seguramente, nunca lleguen a ocuparse, pero la ventaja de los campos VARCHAR de MySQL es, como sabes, que si no se ocupa el espacio máximo, tampoco lo reserva, con lo que no aumenta innecesariamente el tamaño de la tabla. La estructura en SQL queda así:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
-- -- Estructura de tabla para la tabla socios -- CREATE TABLE socios ( id int(11) NOT NULL, doi varchar(255) NOT NULL, nombre varchar(255) NOT NULL, fecha_de_ingreso date NOT NULL, genero enum('H','M') NOT NULL, avatar varchar(255) NOT NULL DEFAULT 'sin_avatar.jpg', activo enum('S','N') NOT NULL DEFAULT 'S' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- -- Indices de la tabla socios -- ALTER TABLE socios ADD PRIMARY KEY (id), ADD UNIQUE KEY index_doi (doi(191)) USING BTREE, ADD KEY nombre_index (nombre(191)), ADD KEY fecha_de_ingreso_index (fecha_de_ingreso), ADD KEY genero_index (genero), ADD KEY avatar_index (avatar(191)), ADD KEY activo_index (activo); -- -- AUTO_INCREMENT de la tabla socios -- ALTER TABLE socios MODIFY id int(11) NOT NULL AUTO_INCREMENT; COMMIT; |
LA LISTA DE USUARIOS
En la base de datos hemos creado una lista de cuatro usuarios ficticios para empezar construyendo nuestra aplicación por el componente encargado de mostrar esta lista. Una vez montado, su aspecto es el siguiente:
En realidad, el aspecto no es lo que nos interesa. Este es sólo bootstrap, que usaremos con más o menos acierto para hacer nuestra vista medianamente atractiva y usable. Lo que realmente nos importa, y es en lo que vamos a centrarnos, es la funcionalidad. Antes de que sigas adelante, déjame decirte que, en esta primera fase, los botones de nuevo y de editar los registros no funcionan aún (no hacen nada). Esto es porque estos botones deberán conducirnos a otro componente del que, de momento, no hemos tocado nada. Están ahí, pero no hacen nada aún.
Los que si son ya perfectamente funcionales son los botones destinados a cambiar la ordenación, y los de eliminar registros. Vamos a ver la operativa.
EL SERVICIO
Aunque en el artículo anterior creamos dos servicios, en este componente usamos sólo uno de ellos: HttpConnectService
. Este servicio contendrá todas las llamadas a API’s que necesite el MembersModule
pero, por ahora, sólo contiene las que necesita el componente MembersListComponent
. Son dos: la encargada de leer la lista de usuarios, y la que borra un usuario cuando se solicita desde la vista. El código queda así:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
import { Injectable } from '@angular/core'; /* Importamos las clases Observable y HttpClient que necesitaremos para comunicar con las API's y recuperar sus resultados. */ import { Observable } from 'rxjs/Observable'; import { HttpClient } from '@angular/common/http'; /* Importamos los environments, para determinar la URL base de las API's */ import { environment } from '../../../environments/environment'; /* IMportamos al clase Member para poder gestionar objetos de esta clase en las llamadas y en los Observables */ import { Member } from '../classes/member'; @Injectable() export class HttpConnectService { /* Leemos la URL base de las API's a partir del archivo de configuración. */ private URL = environment.APIS_URL; /* En el constructor creamos el objeto http de la clase HttpClient, que estará disponible en toda la clase del servicio. */ constructor(private http: HttpClient) {} /* El siguiente método lee los registros y obtiene un observable con la matriz de objetos Member. */ public readRecords$(criterio, orden): Observable<Member[]> { return this.http.get<Member[]>( this.URL + 'read_records.php?criterio=' + criterio + '&orden=' + orden ); } /* El siguiente método llama a la API de borrado pasándole un id. El observable vuelve vació, pero debe estar declarado. */ public deleteRecord$(id: string): Observable<any> { return this.http.get(this.URL + 'delete_record.php?id=' + id); } } |
Como ves, en esta ocasión en el servicio solo hemos incluido los dos métodos de conexión con API’s que obtienen (o no) un observable, pero no lo procesamos aquí. Eso lo hacemos en la lógica del componente (members-list.component.ts
):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 |
import { Component, OnInit } from '@angular/core'; /* Importamos la clase Member que necesitaremos para gestionar los objetos de los usuarios. */ import { Member } from '../classes/member'; /* Importamos el servicio de conexión con API's, necesario para recuperar la lista de usuarios. */ import { HttpConnectService } from '../services/http-connect.service'; /* Para identificar si la respuesta recibida del servidor es de tipo HttpErrorResponse, cuando se produzca un error caturado. */ import { HttpErrorResponse } from '@angular/common/http'; // Declaramos las variables para jQuery declare var jQuery: any; declare var $: any; @Component({ selector: 'aca-members-list', templateUrl: './members-list.component.html', styleUrls: ['./members-list.component.css'] }) export class MembersListComponent implements OnInit { /* DECLARAMOS TODAS LAS VARIABLES QUE VAMOS A USAR EN ESTE COMPONENTE. EL HECHO DE DECLARARLAS COMO public ES PORQUE SERÁN USADAS FUERA DE ESTE SCRIPT. ALGUNAS SE USARÁN EN LA VISTA Y OTRAS EN EL SERVICIO. */ public MembersArray: Member[]; // La matriz donde se alojará la lista de usuarios. public NumberOfMembers: number; // El número de miembros disponible public criterio = '1'; // Para seleccionar el criterio de ordenación. Por defecto 1 es el id public orden = '5'; // El sentido de ordenación. Por defecto es 5 (Ascendente). public nombreParaBorrar: string; // El nombre que se muestra cuando se va a borrar un registro. public idParaBorrar: string; // El id del registro que se va a borrar. public mensajeDeErrorDeAPI = false; // Para activar un mensaje de error cuando se produzca. public mensajeProcesoExitoso = false; // Para avisar cuando se ha finalizado algún proceso con éxito. public mensajeDetalladoDeError = ''; // Para mostrar detalles de un error producido /* En el constructor creamos el objeto connectService, de la clase HttpConnectService, que contiene el servicio mencionado, y estará disponible en toda la clase de este componente. El objeto es private, porque no se usará fuera de este componente. */ constructor(private connectService: HttpConnectService) { this.readRecords(); // Leemos los regstros. } /* Al inicio del componente se llevan a cabo varias operaciones: - Establecemos el título para la pestaña del navegador. - Definimos las etiquetas de la barra de navegación que tendrán la clase active. - Declaramos las propiedades iniciames del modal que se activará para pedir confirmación cuando se solicite el borrado de un miembro. */ ngOnInit() { $(document).prop('title', 'Lista de miembros'); $('.menuLink').removeClass('active'); $('#MembersMenuLink').addClass('active'); $('#MembersListMenuLink').addClass('active'); $('#modalBorrar').modal({ backdrop: false, keyboard: false, show: false }); } /* El siguiente método llama al servicio, para obtener el observable con la lista de registros. Observa que le pasamos el criterio y el sentido de ordenación. Nos suscribe al observable obtenido y, en base al resultado, ejecuta un método u otro. */ private readRecords() { this.connectService .readRecords$(this.criterio, this.orden) .subscribe( this.readSuccess.bind(this), this.catchError.bind(this) ); } /* El siguiente método se ejecuta si readRecords ha funcionado bien. Como al suscribirnos ya hemos "abierto" el observable, ponemos el resultado en la matriz de usuarios, que está bindeada en la vista. No hace falta convertir la lista a partir del JSON que obtuvo el observable, porque la suscripción ya convierte ese JSON en una matriz. */ private readSuccess(membersList) { this.MembersArray = membersList; this.NumberOfMembers = this.MembersArray.length; } /* El siguiente método está bindeado desde los botones de criterio y sentido de ordenación de la vista. Lo que hace es marcar de forma visbible (mediante clases de bootstrap) el botón pulsado, y actualizar las variables criterio y orden. Al terminar, invoca de nievo al método de lectura, con los nuevos valores de las variables mencionadas, para que en la vista se renderice la lista con los criterios elegidos. */ public updateOrder(event) { const botonPulsado = event.srcElement.id; if ( botonPulsado === '1' || botonPulsado === '2' || botonPulsado === '3' || botonPulsado === '4' ) { // El botón pulsado es de criterio de ordenación. $('.criteriaButton') .removeClass('btn-warning') .addClass('btn-default'); $('#' + botonPulsado) .removeClass('btn-default') .addClass('btn-warning'); this.criterio = botonPulsado; } else { // El botón es de sentido de ordenación, no de criterio. $('.orderButton') .removeClass('btn-warning') .addClass('btn-default'); $('#' + botonPulsado) .removeClass('btn-default') .addClass('btn-warning'); this.orden = botonPulsado; } this.readRecords(); } /* El siguiente método está biendado desde los botones de borrar registros. Cuando se pulsa uno de estos botones se activa un modal pidiendo confirmación para efectuar el borrado. */ public preavisoDeBorrado(member) { this.nombreParaBorrar = member.nombre; this.idParaBorrar = member.id; $('#modalBorrar').modal('show'); } /* Si se cancela el borrado desde el modal de preaviso. */ public anularBorrado() { this.nombreParaBorrar = undefined; this.idParaBorrar = undefined; $('#modalBorrar').modal('hide'); } /* Si se confirma el borrado, llamamos al método del servicio que se encarga de conectar con la API de borrado, pasándole el id del registro que queremos borrar. */ public confirmarBorrado(id: string) { this.connectService .deleteRecord$(id) .subscribe( this.deleteSuccess.bind(this), this.catchError.bind(this) ); } /* Si se ha conseguido borrar el registro, se informa de ello y se actualiza la lista de miembros en la vista. Además, se cierra el modal. */ public deleteSuccess() { this.readRecords(); $('#modalBorrar').modal('hide'); this.mensajeProcesoExitoso = true; } /* El siguiente método activa el mensaje de error de API's */ public catchError(err) { this.mensajeDeErrorDeAPI = true; if (err instanceof HttpErrorResponse) { this.mensajeDetalladoDeError += 'Status: ' + err.status + '. '; this.mensajeDetalladoDeError += 'Status text: ' + err.statusText + '. '; this.mensajeDetalladoDeError += 'Error: ' + err.message + '. '; } this.mensajeDetalladoDeError += 'Contacte con el Administrador.'; } /* El siguiente mensaje desactiva el mensaje de error de API's */ public cleanOperationMessages() { this.mensajeDeErrorDeAPI = false; this.mensajeProcesoExitoso = false; } } |
Aquí ves que en la lógica del componente hemos incluido todos los métodos necesarios para la gestión de ese componente. El código es muy fácil de analizar, por dos razones: la primera es que no hay, realmente, nada nuevo. La segunda es que lo he llenado de comentarios para que sepas lo que hace cada método y por qué está ahí. Te sugiero que lo revises, junto con la vista y el css, porque en el próximo artículo cambiaremos alguna cosa, y seguiremos consolidando conocimientos.
CONCLUYENDO
En este artículo hemos preparado la lista de usuarios registrados en un componente específico, y hemos separado la lógica en dos partes: el servicio del móduo y el propio TypeScript del componente. La vista y el css no los he listado aquí porque, realmente, no aportan nada que no sepas ya. Hasta ahora estamos usando lo mismo que ya conocemos, con más o menos adornos, o colocando las cosas en uno u otro sitio. La aplicación, en su estado actual, te la puedes descargar en este enlace. En el próximo artículo vamos a seguir ampliando este CRUD, viendo detalles interesantes.