En este artículo vamos a ver como creamos el formulario para los usuarios. Y vamos a hacerlo interesante. El formulario será el mismo para el alta de nuevos usuarios y para la edición de usuarios existentes. Sin embargo, se comportará de forma diferente según el proceso seleccionado.
Esto nos «obliga» a modificar parte del código que ya tenemos, y nos permite ver conocimientos ya aprendidos pero desde un prisma nuevo, lo que nos permitirá afianzar esos conocimientos, y ver nuevas formas de hacer las cosas en Angular. El objetivo es que te sientas seguro, sepas lo que haces, y por donde te mueves.
En concreto, modificaremos el enrutador secundario, y crearemos un formulario versátil. Aprenderemos más sobre formularios cuando hablemos de los reactive forms, pero cada cosa a su tiempo.
En este artículo vamos a implementar las funcionalidades para que se den de alta nuevos miembros. Dejaremos el componente del formulario de tal modo que quedará totalmente opetrativo para nuevas altas. En cambio, para edición, dejearemos algunos detalles prepararados, pero el formulario estará incompleto, y no pondremos aún toda la operativa. Esto es así porque es bastante materia, y vamos a reservar la parte de edición para un artículo posterior, a fin de ir viendo las cosas poco a poco.
EL ENRUTADOR DE MembersModule
Este archivo (members-routing.module.ts
) es el primero que vamos a tocar. Su nuevo código nos 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 |
import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { MembersListComponent } from './members-list/members-list.component'; import { MemberFormComponent } from './member-form/member-form.component'; const routes: Routes = [ { path: '', component: MembersListComponent }, { path: 'form', component: MemberFormComponent }, { path: 'form/:id', component: MemberFormComponent } ]; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule] }) export class MembersRoutingModule { } |
Observa las líneas resaltadas. Date cuenta que hay dos rutas que nos llevan a MemberFormComponent
. La primera es accesible a través de form
, sin más. Será la que se acceda cuando pulsemos el enlace de grabar un nuevo miembro en la barra de navegación, o el botón correspondiente a la misma función en la vista de la lista de miembros. Ese botón en la lista de miembros es, en realidad, un enlace, así:
<a routerLink="form" class="btn btn-primary">Nuevo</a>
La segunda ruta es accesible a través de form/:id
. Nos lleva al mismo componente pero, además, le pasa un parámetro, al que hemos llamado id Observa los dos puntos en la sintaxis. El paso de parámetros por la URL es algo con lo que ya tuvimos un escarceo en este artículo anterior y en este otro artículo. Ahora vamos a consolidar el uso de esta técnica. A esta ruta se llega a través de los botones de edición, en la lista de usuarios que, finalmente, quedan definidos así:
<a routerLink="form/{{ Member.id }}"><span class="btn btn-warning glyphicon glyphicon-pencil">
EL COMPONENTE MemberFormComponent
Al margen del enrutador, cuya adaptación es, como acabas de ver, extremadamente simple, este es el componente clave sobre el que vamos a trabajar. Básicamente, será un formulario que podrá cumplir dos misiones: crear un nuevo miembro o editar uno existente. Por lo tanto, habrá tres puntos en donde deberemos trabajar:
- La vista del componente (
member-form.component.html
). El formulario deberá presentarse de diferente forma, según lo que vayamos a hacer (por ejemplo, cambiando el rótulo que se muestra, y algún otro detalle). - La lógica del componente (
member-form.component.ts
). Aquí también hay métodos que cambian según se trate de una edición u un alta nueva. Algunos métodos específicos sólo se usan en uno de los dos procesos. - El servicio de conexión (
http-connect.service.ts
). Este servicio, desde el componente de formularios, es usado para conectar con la API correspondiente.
Cómo ya he comentado, en este artículo vamos a centrarnos en la creación de un nuevo miembro. Aún así, los archivos que hemos mencionado tendrán algunos «toques» que nos revelan que, más adelante, los vamos a ampliar para la edición.
LA LÓGICA DEL COMPONENTE
Como ya sabes, aquí es donde ocurre todo. Te voy a reproducir el código, en su estado actual, con gran profusión de comentarios para que sepas que hace cada método, y porqué está ahí:
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 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 |
import { Component, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { HttpConnectService } from '../services/http-connect.service'; import { Member } from '../classes/member'; import * as moment from 'moment'; // Declaramos las variables para jQuery declare var jQuery: any; declare var $: any; @Component({ selector: 'aca-member-form', templateUrl: './member-form.component.html', styleUrls: ['./member-form.component.css'] }) export class MemberFormComponent implements OnInit { public idRecibido: string; // Si edición, el id de miembro. Si nuevo, undefined public member: Member; // Objeto para los datos del miembro // Datos de miembro public doi: string; // Enlace con el formulario public nombre: string; // Enlace con el formulario public doiGrabar: string; // Para procesar sin espacios public nombreGrabar: string; // Para procesar sin espacios public fecha_de_ingreso: string; // Enlace con el formulario public genero: string; // Enlace con el formulario public avatar: string; // Enlace con el formulario public avatarFile: any; // El fichero del avatar procedente del campo file public imageToShow: string; // El avatar en base64, si lo hay (enlaza con el src del formulario) public activo: string; // Enlace con el formulario public dataPack: FormData; // Empaquetado de todo (inc. avatar) para mandar a grabar. /* Flags de resultados. Determinarán que en el formulario se muestren mensajes con los resultados de la operación. */ public errorEnAvatar = false; public errorEnDoi = false; public doiRepetido = false; public errorEnNombre = false; public errorEnFecha = false; public errorEnGenero = false; public errorDeProceso = false; public registroGrabado = false; /* En la firma del constructor creamos un objeto de la clase HttpConnectService, para poder acceder a las conexiones con API's. También creamos un objeto de la clase ActivatedRoute, porque si llegamos hasta aquí desde edición debemos poder recuperar de la ruta el id del usuario a editar. */ constructor( private connectService: HttpConnectService, private ruta: ActivatedRoute ) { this.idRecibido = this.ruta.snapshot.params['id']; /* Si hay un id recibido estamos en edición, por lo que lo primero es leer los datos del miembro que vamos a editar. */ if (this.idRecibido !== undefined) { this.readActualData(); // Leer los datos actuales del miembro, si estamos en edición. } else { /* Para nuevo miembro, se muestra, por defecto, el avatar genérico, y se establece la fecha de alta como la del día en curso. */ this.imageToShow = this.connectService.URL + 'avatares/sin_avatar.jpg'; this.setActualDate(); } } ngOnInit() { $(document).prop('title', 'Formulario de miembro'); $('.menuLink').removeClass('active'); /* Si es un nuevo miembro se activan las etiquetas de la barra de menús. En edición, no las activo, por criterio personal. */ if (this.idRecibido === undefined) { $('#MembersMenuLink').addClass('active'); $('#MemberFormMenuLink').addClass('active'); } } /* Cuando se pulsa el botón de grabación, se hace una validación previa de los datos. Si cumplen, se graba. Si no, se avisa del fallo. */ public checkRecord() { this.clearNotice(); this.doiGrabar = (this.doi === undefined || this.doi === null) ? '' : this.doi; this.nombreGrabar = (this.nombre === undefined || this.nombre === null) ? '' : this.nombre; this.doiGrabar = this.doiGrabar.trim(); this.nombreGrabar = this.nombreGrabar.trim(); if (this.doiGrabar === '' || this.doiGrabar === undefined) { this.errorEnDoi = true; } else if (this.nombreGrabar === '' || this.nombreGrabar === undefined) { this.errorEnNombre = true; } else if (this.fecha_de_ingreso === '') { this.errorEnFecha = true; } else if (this.genero === '' || this.genero === undefined) { this.errorEnGenero = true; } else if (!this.errorEnAvatar) { /* Si no hay errores, y tampoco en el avatar Se graba el registro. Para nuevas altas, se pone '0' como id del usuario, para cumplir con la firma del constructor de la clase. Será la API la que si encuentra '0' sepa que es un nuevo registro, y si encuentra otro valor sepa que es una edición. */ this.member = new Member( '0', this.doiGrabar, this.nombreGrabar, this.fecha_de_ingreso, this.genero ); this.buildDataPack(); // Empaquetamos todo (datos y avatar) en un solo objeto. this.saveRecord(); // Para grabar el objeto. } } /* El método que empaqueta datos y avatar en un objeto FormData */ private buildDataPack() { this.dataPack = new FormData(); this.dataPack.append('User', JSON.stringify(this.member)); this.dataPack.append('Avatar', this.avatarFile); } /* Reservado para cuando se monte la edición. */ private readActualData() {} /* Creado el objeto FormData se le pasa al servicio que lo envia a la API. */ private saveRecord() { this.connectService .saveRecod$(this.dataPack) .subscribe(this.saveSuccess.bind(this), this.catchError.bind(this)); } /* Si el proceso de alta o edición ha finalizado correctamente. */ private saveSuccess(result) { if (result === '0') { this.registroGrabado = true; } else if (result === '23000') { this.doiRepetido = true; } else if (result === '42S22') { this.errorDeProceso = true; } this.restoreData(); } /* Si hay errores en las llamadas a las API's */ private catchError() { this.errorDeProceso = true; } /* Se borran las notificaciones que se hayan podido producir. Este método se desencadena cuando se pone el foco en algún campo, o se pulsa el botón de envío o de reset. */ public clearNotice() { this.errorEnDoi = false; this.doiRepetido = false; this.errorEnNombre = false; this.errorEnFecha = false; this.errorEnGenero = false; this.errorDeProceso = false; this.registroGrabado = false; } public selectAvatarToShow() { if (this.avatar === undefined || this.avatar === '') { this.imageToShow = this.connectService.URL + 'avatares/sin_avatar.jpg'; this.errorEnAvatar = false; } else { this.avatarFile = $('#AvatarFile')[0].files[0]; if ( this.avatarFile['type'] !== 'image/jpeg' || this.avatarFile['size'] > 40960 ) { this.errorEnAvatar = true; } else { const reader: FileReader = new FileReader(); reader.onloadend = () => { this.imageToShow = reader.result; }; reader.readAsDataURL(this.avatarFile); this.errorEnAvatar = false; } } } /* Cuando se pulsa un botón de género se asigna su valor a la variable correspondiente. */ public checkGender(gender) { this.genero = gender; } /* Cuando es un alta nueva se usa para poner como fecha de inscripción la del día. */ private setActualDate() { this.fecha_de_ingreso = moment().format('YYYY-MM-DD'); } /* Restauramos los campos del formulario como respuesta a una grabación correcta. Aquí no se llama a this.clearNotice(), para que permanezca el mensaje de grabación correcta. */ private restoreData() { this.doiGrabar = undefined; // Para procesar sin espacios this.nombreGrabar = undefined; // Para procesar sin espacios this.imageToShow = this.connectService.URL + 'avatares/sin_avatar.jpg'; this.activo = undefined; this.dataPack = new FormData(); $('#MemberDataForm')[0].reset(); this.setActualDate(); } /* El siguiente método es invocado por el botón de reset del formulario. */ public restoreMemberForm() { this.clearNotice(); this.restoreData(); } } |
Las primeras líneas no aportan nada nuevo. Son las importaciones que ya sabemos que tenemos que hacer. La mayoría son comunes a casi cualquier componente de cualquier módulo.
Dentro de la clase empezamos a ver lo que necesitamos. En primer lugar, en la línea 17
declaramos una variable llamada idRecibido
. La declaramos como de tipo string
. Si llegamos a este formulario desde edición, en esta variable se almacenará, un poco más adelante, el id del registro que queremos editar. Si llegamos desde la opción de crear una nueva alta, esta variable permanecerá como undefined
durante todo el ciclo de vida del componente.
En las líneas de la 18
a la 30
se declaran todas las variables que va a usar el formulario para los datos del miembro que vamos a crear o editar. Aquí se declaran las variables individuales para cada dato, así como una variable para el objeto de la clase Member
. También se declara un objeto de la clase FormData
, para empaquetarlo todo antes de mandarlo al servicio que lo pasará a la API. Quizá te llamen la atención las variables doiGrabar
y nombreGrabar
que hemos declarado en las líneas 22
y 23
. Cuando vayamos a grabar un registro, una de las cosas que tenemos que asegurarnos es que no se hayan rellenado estos campos con espacios en blanco. La aplicación no permite dejarlos vacíos, pero algún usuario «patoso» podría rellenarlos con espacios en blanco. Sin embargo, no podemos aplicar el método trim()
directamente a las variables doi
y nombre
, porque estas enlazan con el formulario mediante [(ngModel)]
, lo que daría lugar a un error. Por lo tanto, una vez que se pide enviar el formulario, los valores de doi
y nombre
pasan a doiGrabar
y nombreGrabar
, sobre las que sí podemos aplicar el método trim()
. Estas serán las que, finalmente, se empaqueten en el objeto.
Las variables booleanas que se declaran entre las líneas 34
y 41
se emplean para mostrar u ocultar mensajes al usuario, en el formulario, según se haya producido algún error en el proceso, o si todo ha ido bien y el nuevo usuario se ha grabado correctamente.
En la firma del constructor declaramos dos objetos (que, por estár declarados en la firma ya sabemos que estarán disponibles para toda la clase, como si se hubieran declarado previamente). Uno es de la clase HttpConnectService
, y lo usaremos para conectar con el servicio que envía los datos a las API’s. El otro es de la clase ActivatedRoute
. ActivatedRoute
es, realmente, otro servicio, solo que este no le hemos creado nosotros. Es propio de Angular y se usa, entre otras cosas, para identificar los parámetros pasados por la ruta, si los hay. No nos haría falta si este componente sólo se fuera a usar para nuevas altas pero, como nos hará falta para identificar el id de un usuario cuando hagamos la parte de edición, pues ya lo dejamos importado. Dentro del constructor, en la línea 51
, usamos el objeto ruta
, de la clase ActivatedRoute
, para identificar el parámetro id
, si lo hay. Lo identifica a partir de la captura de la URL (snapshot
). Esta es la captura, digamos, en una «foto» de la ruta tal como se ha llamado. Es decir. Si, por alguna razón, en la URL «aparece» algo nuevo, snapshot
no lo detectará. Esto se podría dar en determinados casos de los que ya hablaremos. Por ahora, nos basta saber que el snapshot
es justo lo que necesitamos. Y dentro de este, la matriz params
contiene los parámetros que hay en la ruta. Como buscamos el parámetro id
, si estamos haciendo la llamada para un alta nueva, este no existe, por lo que obtendremos undefined
. Lo que leamos de este parámetro pasa a idRecibido. Si hay algo que no sea undefined
(una edición), se ejecutará el método readActualData()
. Como es para la edición, ese método está declarado, pero aún no está construido. En caso de alta nueva, se establece una fecha por defecto para la fecha de inscripción, y se muestra, como avatar, el icono sin_avatar.jpg
.
El resto de los métodos ya nos son familiares, del anterior CRUD que hicimos, por lo que no vamos a detenernos en ellos más allá de los comentarios del código. Lo que quizá pueda llamarte la atención es la forma de resetear el formulario en la línea 197
:
$('#MemberDataForm')[0].reset();
Observa que referenciamos el formulariuo a través de jQuery. Sin embargo, ves el índice [0]
, que no cabría esperar en una instrucción como esta. Eso es porque Angular «envuelve» el formulario en una capa adicional. Con el uso de ese índice, lo «sacamos» de esa envoltura, para poder aplicarle el método reset()
.
CONCLUYENDO
En este artículo hemos aprendido a organizar todo el código, y dejarlo medio preparado para una futura ampliación y, lo que es más importante, hemos afianzado conocimientos que vamos a necesitar tener muy presentes en cualquier proyecto. La aplicación, en su estado actual, puedes descargarla en este enlace. En el próximo artículo seguiremos aprendiendo cosas nuevas con este CRUD.