Resumen
Angular es uno de los frameworks Javascript más utilizados en el momento junto a React y Vue.js. Angular ofrece una solución completa para el manejo de rutas, formularios, inyección de dependencias y building, entre otros. Además, el uso en UAL-STIC de NestJS para backend, el cual tiene una arquitectura muy basada en Angular, ha propiciado que sea la tecnología actual elegida en UAL-STIC para el desarrollo frontend. Así, tenemos la arquitectura de Angular tanto en frontend como en backend, facilitando a los equipos de desarrollo el cambio entre desarrollo frontend y backend.
En este tutorial se abordan los principales temas y desafíos con los que nos solemos encontrar en cada aplicación en el ámbito de gestión. Se hace a través del desarrollo de una aplicación de ejemplo sencilla y se tratan desde temas metodológicos, como la organización de carpetas y archivos del proyecto, hasta los temas particulares del desarrollo de aplicaciones CRUD, como formularios reactivos, manejo de rutas, paso de datos entre componentes e interacción con servicios REST.
-
Aprender las técnicas básicas y habituales de Angular mediante el desarrollo de una aplicación CRUD.
-
Definir una estructura de carpetas y archivos adecuada para el desarrollo y posterior mantenimiento.
-
Usar las rutas para la navegación de la aplicación y la selección de componentes.
-
Crear servicios que interactúen con una API REST.
-
Manejar correctamente la asincronía de los servicios REST.
-
Crear y utilizar formularios reactivos y validadores de datos.
-
Compartir datos entre componentes.
-
Crear páginas con opciones de filtrado.
-
Crear cuadros de diálogo informativos y de validación.
-
Desarrollar una aplicación Angular sin necesidad de tener un backend definitivo.
-
Utilizar la técnica del lazy loading.
-
Usar variables de entorno.
-
Utilizar Angular Material para la intefaz de la aplicación.
-
Utilizar Angular Flex-Layout para la organización de elementos de la interfaz de usuario.
Disponible el repositorio usado en este tutorial. |
1. Introducción
Angular es un framework JavaScript open source mantenido por Google. Es uno de los frameworks Javascript más utilizados en el momento junto a React y Vue.js. Angular ofrece una solución completa para el manejo de rutas, formularios, inyección de dependencias y building, entre otros. Dado que en UAL-STIC se usa NestJS para backend, el cual tiene una arquitectura muy basada en Angular e Ionic para la APP, Angular es la tecnología más adeacuada para desarrollo frontend en UAL-STIC. De esta forma, tenemos la arquitectura de Angular tanto en frontend como en backend y en la APP.
En este tutorial seguiremos los pasos para el desarrollo de una aplicación sencilla de gestión de espacios con el objeto de introducir los principales componentes y técnicas que se siguen en el desarrollo de una aplicación Angular. Trataremos la creación de una aplicación como un conjunto de componentes que se pueden comunicar entre sí, el uso de mecanismos de routing para la presentación de componentes en función de la URL, uso de servicios, separación de la lógica de la presentación, manejo de formularios reactivos y uso de Angular Material.
Se trata sólo de una introducción a aspectos básicos. No obstante, temas tan interesantes, útiles y necesarios como la autenticación, el uso de guardas para la protección de rutas, interceptores, logging y testing, entre otros, no están presentes en este tutorial. En tutoriales posteriores se irán tratando estos y otros temas de interés.
La aplicación a desarrollar ofrece básicamente dos funcionalidades:
-
Una relacionada con la consulta de espacios disponibles para reservar. Ofrecerá una lista de espacios con cada una de las reservas asignadas indicando para cada una de ellas el edificio y aula que ocupa, su fecha, hora, descripción, asignatura y profesor. Se permitirá su consulta mediante cada una de esas características.
-
Otra relacionada con la gestión de solicitudes. Permitirá las cuatro operaciones CRUD de creación, consulta, actualización y eliminación de solicitudes. De las solicitudes se guardarán datos personales de la persona que realiza la solicitud, el tipo de actividad, y el horario de la reserva. En el Anexo II. Prototipo de la aplicación se muestra un prototipo de la aplicación y los comandos y navegación entre pantallas.
2. Creación de la aplicación
El objetivo de esta sección es crear el proyecto de la aplicación junto con las librerías que vaya a usar.
2.1. Creación de la aplicación con el CLI de Angular
Comenzamos creando la aplicación con el CLI de Angular.
$ ng new angular-espacios
? Would you like to add Angular routing? Yes (1)
? Which stylesheet format would you like to use? CSS
1 | Indicamos que queremos que genere el archivo de routing. |
2.2. Instalación de librerías
En este proyecto usaremos las librerías siguientes:
-
Angular Material como librería de componentes de la interfaz de usuario. Nos proporcionará botones, tarjetas, desplegables para selección de fechas (date-pickers), cuadros de diálogo, barras para presentar mensajes (snackbars) y demás.
-
Angular Flex-Layout es una librería para la organización o disposición de componentes en la pantalla.
Podíamos haber usado Bootstrap para la organización de los componentes en las pantalla de la aplicación. Sin embargo, usaremos Angular Flex-Layout porque es lo que usa Fuse Angular, el template que se usa actualmente para el desarrollo de las aplicaciones de UAL-STIC. |
Para la instalación de Angular Material ejecutaremos el comando siguiente eligiendo las opciones por defecto, salvo en la de los tipos, que seleccionaremos que sí para usar los estilos tipográficos de Angular.
$ ng add @angular/material
Would you like to proceed? Yes
✔ Package successfully installed.
? Choose a prebuilt theme name, or "custom" for a custom theme: Indigo/Pink [ Preview: https://material.a
ngular.io?theme=indigo-pink ]
? Set up global Angular Material typography styles? Yes
? Set up browser animations for Angular Material? Yes
La instalación de Angular Flex-Layout la haremos con
$ npm i -s @angular/flex-layout @angular/cdk
Para una mayor comodidad a la hora de escribir código, puedes instalar el plugin de Angular Flex-Layout para Visual Studio: https://marketplace.visualstudio.com/items?itemName=1tontech.angular-material |
3. Creación del scaffolding de la aplicación
En esta sección crearemos los módulos, componentes y rutas que tendrá la aplicación. La aplicación constará de una barra lateral a la izquierda, un pie inferior y una zona central donde ser presentarán las pantallas de la aplicación. La figura siguiente ilustra la disposición de estos elementos.
3.1. Creación del módulo shared
Comenzaremos creando un módulo al que denominaremos shared
donde incluiremos todos los componentes compartidos de la aplicación, como son las dos barras laterales. En breve crearemos los componentes de las barras.
$ ng g module shared
3.2. Actualización de los módulos de app.module.ts
Para que el módulo shared
pueda ser usado desde el componente de la aplicación (app.component
), hay que incluir el módulo shared
en app.module.ts
. También incluiremos en app-module.ts
el módulo de Flex-Layout que usaremos para la distribución de elementos en la aplicación. A continuación se muestran los cambios introducidos en app.module.ts
para importar SharedModule
y FlexLayoutModule
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { SharedModule } from './shared/shared.module';
import { FlexLayoutModule } from '@angular/flex-layout';
@NgModule({
declarations: [AppComponent],
imports: [
BrowserModule,
AppRoutingModule, (1)
BrowserAnimationsModule, (2)
FlexLayoutModule, (2)
SharedModule, (3)
],
providers: [],
bootstrap: [AppComponent],
})
export class AppModule {}
1 | Módulo incluido automáticamente por el CLI de Angular al crear el proyecto con la opción de Angular routing. |
2 | Módulo de efectos visuales introducido por nuestras nuevas librerías |
3 | Módulo de Flex-Layout |
4 | Módulo shared |
3.3. Creación de las barras laterales
Para cada barra crearemos un componente, al que incluiremos dentro del componente shared
.
$ ng g component shared/sidebar
$ ng g component shared/footbar
Al crear los componentes dentro de la carpeta del módulo shared
, el CLI de Angular incluirá los componentes en declarations
, indicando que son componentes del módulo y que podrán referenciarse entre ellos. Sin embargo, dichos compomentes aún no podrán ser utilizados por otros componentes o por otros módulos aunque incluyan al módulo shared
.
Para que un componente pueda ser usado fuera del módulo en el que está definido, debe incluirse en el módulo |
A continuación, modificaremos el módulo shared/shared.module.ts
para exportar los componentes de las barras laterales y de pie, de forma que se puedan usar fuera de su módulo.
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SidebarComponent } from './sidebar/sidebar.component';
import { FootbarComponent } from './footbar/footbar.component';
@NgModule({
declarations: [SidebarComponent, FootbarComponent],
imports: [CommonModule],
exports: [SidebarComponent, FootbarComponent], (1)
})
export class SharedModule {}
1 | Incluir los componentes en el módulo para que puedan ser usados por otros módulos |
3.4. Configuración del layout de la aplicación
Como hemos comentado, crearemos un layout en el que tenemos una disposición horizontal con la barra laterial la izquierda y un bloque de dos componentes verticales a continuación.
Definiremos este layout en app.component.html
<div fxLayout="row" fxLayoutAlign="start stretch" fxFill> (1)
<div>
<app-sidebar></app-sidebar>
</div>
<div fxFlex fxLayout="column">
<div fxFlex>
<router-outlet></router-outlet>
</div>
<div>
<app-footbar></app-footbar>
</div>
</div>
</div>
1 | Disposición de componenes en fila. Alineación desde el inicio en horizontal y ocupando todo en vertical |
2 | Disposición de componentes en columna |
3.5. Creación de un módulo para los módulos de Angular Material
La aplicación de este tutorial usa varios componentes de Angular, como botones, cuadros de diálogo, un módulo de calendario, barra de presentación de mensajes y demás. Lo más adecuado y eficiente es hacer que cada módulo de la aplicación sólo importe los módulos de los componentes Material que va a utilizar. Sin embargo, en este tutorial, por comodidad y facilidad crearemos un módulo que denominaremos Material
que exportará todos los módulos de componentes de Angular Material que va a usar la aplicación en su conjunto. Posteriormente, importaremos este módulo desde el resto de módulos de la aplicación. Es cierto que habrá módulos que necesiten todos los módulos de nuestro módulo Material
, mientras que habrá otros que quizá no los usen todos. Como hemos dicho esto no es lo más correcto, pero lo haremos aquí por comodidad.
Para crear el módulo Material
ejecutaremos
$ ng g module material
A continuación se indican los componentes Material que usará la aplicación de este tutorial:
-
Button
para los botones de la aplicación. -
Card
para agrupar elementos en tarjetas. -
DatePicker
para la selección de fechas. -
Dialog
para cuadros de diálogo. -
Form field
para los campos de los formularios. -
Icon
para uso de iconos Material. -
Input
para elementosinput
de los formularios. -
List
para la creación de listas. -
Select
para listas desplegables. -
Snackbar
para barra de mensajes. -
Steeper
para definir un asistente con pasos. -
Table
para presentación de datos en tablas.
También incluiremos el módulo de Flex-Layout
para la distribución de elementos en la pantalla. Así quedaría nuestro módulo angular/angular.module.ts
:
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FlexLayoutModule } from '@angular/flex-layout';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatDatepickerModule } from '@angular/material/datepicker';
import { MatDialogModule } from '@angular/material/dialog';
import { MatExpansionModule } from '@angular/material/expansion';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatListModule } from '@angular/material/list';
import { MatNativeDateModule } from '@angular/material/core';
import { MatSelectModule } from '@angular/material/select';
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { MatStepperModule } from '@angular/material/stepper';
import { MatTableModule } from '@angular/material/table';
@NgModule({
declarations: [],
imports: [CommonModule],
exports: [ (1)
FlexLayoutModule, (2)
MatButtonModule, (3)
MatCardModule,
MatDatepickerModule,
MatDialogModule,
MatExpansionModule,
MatFormFieldModule,
MatIconModule,
MatInputModule,
MatListModule,
MatNativeDateModule,
MatSelectModule,
MatSnackBarModule,
MatStepperModule,
MatTableModule,
],
})
export class MaterialModule {}
1 | Lista de módulos Material exportados para que puedan ser usado al importar este módulo |
2 | Módulo de Flex-Layout |
3 | Módulos Material para nuestra aplicación |
Para una mayor legibilidad del código, se recomienda dejar ordenada las listas de |
3.6. Creación de los módulos de la aplicación
A partir de la organización o descomposición funcional a un primer nivel de la aplicación crearemos los módulos de la aplicación Angular. En el caso de este tutorial, la aplicación va a estar formada funcionalmente por un módulo de solicitudes y un módulo de espacios. Además, se crerá un módulo home
-
El módulo de solicitudes permitirá listar, crear, modificar y eliminar solitudes de espacios.
-
El módulo de espacios permitirá realizar consultas sobre ocupación de espacios.
-
El módulo
home
incluye el componente de inicio de la aplicación, que se mostrará al inicio o al tratar de ir a una ruta no disponible.
A continuación se muestra un diagrama que ilustra esta organización funcional.
Para reducir el tiempo y el tamaño de la carga inicial de la aplicación utilizaremos la técnica de lazy loading.
Crearemos los módulos con estas instrucciones. Incluiremos el parámetro --routing
para que genere un archivo de rutas a nivel de módulo.
$ ng g module main/home --routing
$ ng g module main/solicitudes --routing
$ ng g module main/espacios --routing
Los archivos de rutas a nivel de módulo permiten organizar mejor las rutas de una aplicación. A un nivel general, |
3.7. Creación de los componentes de la aplicación
Un módulo organiza un bloque funcional del dominio de la aplicación (p.e. solicitudes, espacios, …). Los componentes Angular permitirán llevar a cabo la funcionalidad del módulo.
La organización que seguiremos para los componentes de la aplicación podría resumirse de esta forma.
Dentro de la carpeta de cada módulo encontraremos:
-
Un archivo de módulo
-
El archivo de routing del módulo para implementar lazy loading
-
Un directorio
pages
que contendrá a su vez un directorio para las funcionalidades-
consultar
-
crear
-
-
Un directorio
components
que contendrá a su vez un directoriodialogo-eliminar
que incluirá un componente de cuadro de diálogo para la funcionalidad de eliminar.
La figura siguiente ilustra cómo quedaría la carpeta de un módulo:
Crearemos los componentes con estas instrucciones
$ ng g c main/home/pages/home
$ ng g c main/espacios/pages/consultar
$ ng g c main/solicitudes/pages/consultar
$ ng g c main/solicitudes/pages/crear
3.8. Creación de las rutas
Ahora vamos a crear cada una de las rutas permitidas en la aplicación. Una vez creadas, habremos indicado el componente que mostrará la aplicación al ir a cada ruta. En esta sección configuraremos:
-
el archivo
app-routing.module.ts
para hacer lazy loading indicando la ruta raíz de cada bloque funcional de la aplicación (p.e.home
,solicitudes
,espacios
) y la ubicación de la clase del módulo de rutas correspondiente. -
cada uno de los archivos de rutas parciales de cada módulo.
Archivo de rutas desde app-routing.module.ts
:
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
const routes: Routes = [
{
path: 'solicitudes', (1)
loadChildren: () => (2)
import('./main/solicitudes/solicitudes.module').then(
(m) => m.SolicitudesModule
),
},
{
path: 'espacios',
loadChildren: () =>
import('./main/espacios/espacios.module').then((m) => m.EspaciosModule),
},
{
path: '', (3)
loadChildren: () => (4)
import('./main/home/home.module').then((m) => m.HomeModule),
},
{
path: '**', (5)
redirectTo: '',
},
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule],
})
export class AppRoutingModule {}
1 | Configuración de la URL de la ruta de un módulo |
2 | Lazy loading del módulo indicando el archivo y la clase del módulo |
3 | Configuración de la ruta vacía |
4 | Módulo asociado a la ruta vacía (después del resto) |
5 | Expresión regular para indicar que redirija cualquier otro path no indicado al path que consideramos predeterminado (en nuestro caso, el vacío) |
Los |
A continuación, creremos los archivos de rutas de cada módulo de la aplicación. En ellos se indica por un lado la ruta parcial a añadir a la ruta de su módulo global; por otro lado, se indica el componente asociado a la ruta parcial y que se mostrará, por tanto, al activar cada ruta.
Archivo main/home/home.routing.ts
:
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HomeComponent } from './pages/home/home.component';
const routes: Routes = [
{
path: '',
children: [{ path: '', component: HomeComponent }],
},
{
path: '**',
redirectTo: '',
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class HomeRoutingModule {}
Archivo main/espacios/espacios.routing.ts
:
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { ConsultarComponent } from './pages/consultar/consultar.component';
const routes: Routes = [
{
path: '',
children: [
{ path: 'consultar', component: ConsultarComponent },
{ path: '', redirectTo: 'consultar' },
],
},
{
path: '**',
redirectTo: 'consultar',
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class EspaciosRoutingModule {}
Archivo main/solicitudes/solicitudes.routing.ts
:
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { CrearComponent } from './pages/crear/crear.component';
import { ConsultarComponent } from './pages/consultar/consultar.component';
const routes: Routes = [
{
path: '',
children: [
{ path: 'crear', component: CrearComponent },
{ path: 'consultar', component: ConsultarComponent },
{ path: '', redirectTo: 'consultar' },
],
},
{
path: '**',
redirectTo: 'crear',
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class SolicitudesRoutingModule {}
A continuación, comprobaremos que las rutas definidas funcionan correctamente. Escribiremos las siguientes URL en el navegador y deben ser respetadas aunque aún no muestren nada. Es decir, no deben redirigirnos a la ruta predeterminada, señal de que es una ruta incorrecta.
Por contra, las siguientes rutas no serán reconocidas y seremos redirigidos a las rutas predeterminadas de cada módulo:
4. Primeros componentes de la aplicación
4.1. Barra lateral y la barra de pie
Comenzaremos con la configuración del módulo shared
. Como tanto la barra lateral como la de pie usarán componentes de Angular Material, habrá que importar el módulo Material
creado anteriormente.
Además, como la barra lateral hará uso de los routerLink
para cargar en la zona de páginas de la aplicación los componentes seleccionados, también tendrá que importarse RouterModule
.
Así queda shared/shared.module.ts
:
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SidebarComponent } from './sidebar/sidebar.component';
import { FootbarComponent } from './footbar/footbar.component';
import { MaterialModule } from '../material/material.module';
import { RouterModule } from '@angular/router';
@NgModule({
declarations: [SidebarComponent, FootbarComponent],
imports: [CommonModule, MaterialModule, RouterModule], (1)
exports: [SidebarComponent, FootbarComponent],
})
export class SharedModule {}
1 | Importación de los módulos de Material y de routing |
No olvidar añadir |
A continuación se muestra el código de la barra lateral con el menú de operaciones de la aplicación. Se trata de un botón Home
, un desplegable con acciones de solicitudes y un desplegable con acciones para espacios. Posteriormente agregaremos el botón de Créditos.
Archivo shared/sidebar/sidebar.component.html
:
<div fxLayout="column">
<button mat-button routerLink="/">Home</button>
<hr />
<mat-accordion>
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title> Solicitudes </mat-panel-title>
</mat-expansion-panel-header>
<div fxLayout="column">
<div>
<button mat-button routerLink="./solicitudes/crear">Crear</button>
</div>
<div>
<button mat-button routerLink="./solicitudes/consultar">
Consultar
</button>
</div>
</div>
</mat-expansion-panel>
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title> Espacios </mat-panel-title>
</mat-expansion-panel-header>
<div fxLayout="column">
<a mat-button routerLink="./espacios/consultar">Consultar</a>
</div>
</mat-expansion-panel>
</mat-accordion>
</div>
La barra del pie estará formada por tres botones con los enlaces al Aviso legal y a las políticas de privacidad y accesibilidad.
Archivo shared/footbar/footbar.component.html
<div fxLayout="row">
<a mat-button href="https://www.ual.es/avisolegal" target="_blank"
>Aviso legal</a
>
<a mat-button href="https://www.ual.es/politicaprivacidad" target="_blank"
>Política de Privacidad</a
>
<a mat-button href="https://www.ual.es/accesibilidad" target="_blank"
>Política de Accesibilidad</a
>
</div>
4.2. Página de inicio
La página de inicio dará la bienvenida usando componentes Material y permitirá acceder a la consulta de espacios.
Comenzaremos añadiendo el módulo de componentes Material de nuestra aplicación al módulo Home
.
Archivo main/home/home.module.ts
:
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { HomeRoutingModule } from './home-routing.module';
import { HomeComponent } from './pages/home/home.component';
import { MaterialModule } from '../../material/material.module';
@NgModule({
declarations: [HomeComponent],
imports: [CommonModule, HomeRoutingModule, MaterialModule], (1)
})
export class HomeModule {}
1 | Inclusión de nuestro módulo de Material |
Para la página de inicio busca tu propia imagen, colócala en assets/images/
y añade un código como este.
Archivo main/home/pages/home.component.html
:
<div fxLayout="row" fxLayoutAlign="center center">
<mat-card class="text-center" fxLayout="column" fxLayoutAlign="center center">
<img src="assets/images/empty.png" />
<h1>¡Hola! ¿Aún no has reservado ningún espacio?</h1>
<p>
Si deseas reservar un espacio, consulta la disponibilidad a través del
siguiente enlace.
</p>
<button
[routerLink]="['/espacios/consultar']"
mat-stroked-button
ngClass.xs="mat-fab"
color="primary"
>
<span fxHide fxShow.gt-xs>Ver disponibilidad de espacios</span>
</button>
</mat-card>
</div>
Quedará algo así:
4.3. Formularios reactivos
Los formularios reactivos ofrecen una solución limpia a la gestión de los datos que maneja un formulario (p.e. manipulación de su contenido y validación de datos). Por tanto, tendremos la lógica del formulario separada de su presentación.
Para usar formularios reactivos en una aplicación tendremos que seguir estos pasos:
-
Importar el módulo
ReactiveFormsModule
en el módulo en el que esté el formulario. -
En la parte Typescript del formulario:
-
Inyectar
FormBuilder
en el constructor. -
Crear un objeto
FormGroup
que representa al formulario. En él se definen todos los campos que tendrá el formulario.FormGroup
permite la manipulación de los valores de los campos y permite obtener su validez.
En realidad un objeto
FormGroup
está formado por una colección de objetosFormControl
. CadaFormControl
es el objeto TypeScript que representa a un elemento del formulario. Por tanto, elFormGroup
está formado por todos losFormControl
que representan a los elementos HTML del formulario. -
-
En la parte HTML del formulario:
-
En la etiqueta
<form>
de creación del formulario asociarlo con el objetoFormGroup
creado en la parte TypeScript. Esto se hace añadiéndole[formGroup]="<nombre-del-objeto-FormGroup>"
. -
Cada campo está conectado a su
FormControl
mediante un atributoformControlName
-
A continuación se muestra un ejemplo y la correspondencia entre ellos:
Como en el FormControl email
se ha indicado que es requerido, en el formulario aparece la indicación de obligatorio con el asterisco (*).
TypeScript del componente:
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
@Component({
selector: 'app-mi-form',
templateUrl: './mi-form.component.html',
styleUrls: ['./mi-form.component.css'],
})
export class MiFormComponent implements OnInit {
constructor(private fb: FormBuilder) {} (1)
ngOnInit(): void {}
formPersona: FormGroup = this.fb.group({ (2)
nombre: [{ value: 'John Smith', disabled: true }], (3)
email: [, [Validators.required, Validators.email]], (4)
});
}
1 | Inyección de FormBuilder |
2 | Creación del FormGroup que representa al formulario |
3 | FormControl configurado a un valor y desactivado |
4 | FormControl que impone dos validadores de Angular (requerido y formato de email) |
HTML del componente
<form [formGroup]="formPersona" fxLayout="column" fxFlexOffset="10"> (1)
<mat-form-field>
<mat-label>Nombre</mat-label
><input matInput type="text" formControlName="nombre" /> (2)
</mat-form-field>
<mat-form-field>
<mat-label>Email</mat-label
><input matInput type="text" formControlName="email" /> (3)
</mat-form-field>
</form>
1 | Asociación del formulario al objeto FormGroup formPersona de la parte TypeScript |
2 | Asociación al FormControl name mediante formControlName |
3 | Asociación al FormControl email mediante formControlName |
4.4. Formulario de creación de solicitudes
En este tutorial trabajaremos con formularios reactivos. Esto nos permitirá desviar la lógica asociada al formulario a la parte TypeScript del componente y dejar más limpia la parte HTML del componente. Cada objeto del formulario HTML tendrá su homólogo en la parte TypeScript, que permitirá acceder, modificar, y en general, controlar sus datos, quedando así el HTML y el TypeScript del formulario totalmente conectados. Esto supone:
-
Importar
ReactiveFormsModule
en el módulo de solicitudes para poder trabajar con formularios reactivos. -
Crear un objeto formulario en la parte TypeScript del componente.
A continuación se muestra un mock del aspecto deseado del formulario de creación de solicitudes integrado en la aplicación.
Comenzamos con las importaciones al módulo que contiene el componente en el que está nuestro componente de formulario. Como se trata de un formulario reactivo y en el usaremos componentes Material, tendremos que importar el módulo ReactiveFormsModule
y nuestro módulo de uso de componentes Material.
Archivo main/solicitudes/solicitudes.module.ts
:
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SolicitudesRoutingModule } from './solicitudes-routing.module';
import { ConsultarComponent } from './pages/consultar/consultar.component';
import { CrearComponent } from './pages/crear/crear.component';
import { ReactiveFormsModule } from '@angular/forms';
import { MaterialModule } from '../../material/material.module';
@NgModule({
declarations: [ConsultarComponent, CrearComponent],
imports: [
CommonModule,
SolicitudesRoutingModule,
ReactiveFormsModule, (1)
MaterialModule, (2)
],
})
export class SolicitudesModule {}
1 | Módulo de formularios reactivos |
2 | Módulo de los componentes Material de nuestra aplicación |
Si no se importa
|
A continuación crearemos la parte TypeScript del componente de creación de solicitudes. Se trata de:
-
Definir el objeto formulario con los campos que habrá en la pantalla
-
Para cada campo se define si tiene valores predeterminados, si el campo está desactivado y sus validadores.
Definiremos los campos como pares JSON con los nombres del campo, los arrays indicando los valores predeterminados, validadores, si están desactivados, y demás.
Este componente tendrá que implementar un método save
que será llamado por la parte HTML del componente cuando se quiera crear la solicitud. Por ahora será un método que simplemente imprimirá por consola los valores introducidos a modo de comprobación. Posteriormente, llamará a un servicio que crearemos más adelante y que se dedicará a almacenar la solicitud.
Archivo main/solicitudes/pages/solicitudes.component.ts
:
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
@Component({
selector: 'app-crear',
templateUrl: './crear.component.html',
styleUrls: ['./crear.component.css'],
})
export class CrearComponent implements OnInit {
formHorario: FormGroup = this.fb.group({ (1)
nombre: [{ value: '', disabled: true }], (2)
cargo: [, [Validators.required]], (3)
unidad: [{ value: '', disabled: true }],
telefono: [{ value: '', disabled: true }],
email: [, [Validators.required, Validators.email]], (4)
tipo: [,],
nombreActividad: [, [Validators.required, Validators.minLength(5)]], (5)
start: [,],
end: [,],
dia: [,],
horaInicio: [,],
horaFin: [,],
});
cargos: string[] = [ (6)
'Profesor Titular de Universidad',
'Director de Secretariado de Innovación Tecnológica',
];
diasSemana: string[] = [ (7)
'lunes',
'martes',
'miercoles',
'jueves',
'viernes',
'sabado',
'domingo',
];
horas = Array.from(Array(24).keys()); (8)
constructor(private fb: FormBuilder) {} (9)
ngOnInit(): void { (10)
this.formHorario.controls['nombre'].setValue('Manuel Torres Gil');
this.formHorario.controls['unidad'].setValue('Informática');
this.formHorario.controls['telefono'].setValue('84030');
}
save() { (11)
console.log('this.formHorario :>> ', this.formHorario);
}
1 | Los campos son creados en JSON como valores del método group del objeto FormBuilder que representa al formulario. |
2 | Definición del nombre del campo y su configuración (valores predeterminados, validadores, si está desactivado, …) |
3 | Uso del validador Required . Va después del elemento dedicado al valor inicial |
4 | Se puede usar una lista de validadores |
5 | Validador de longitud mínima |
6 | Array para inicializar una lista de cargos de ejemplo |
7 | Array para inicializar la lista de día de la semana en un listbox |
8 | Array para inicializar las horas en un listbox |
9 | Inyección de FormBuilder |
10 | Inicialización de valores |
11 | Método save inicializado con código de prueba |
A continuación crearemos la parte visual del componente. Como hemos comentado, se trata de un formulario reactivo ligado al objeto formHorario
creado en la parte TypeScript. Tal y como se mostró en la figura del mock del formulario de creación de solicitudes, organizaremos sus elementos en tres tarjetas (Datos personales, Datos de la actividad, Horario de la reserva). Para la selección de fechas usaremos un componente Datepicker
de Material. Las horas las seleccionaremos mediante listas desplegables. Se trata de la primera aproximación al formulario. Por ahora:
-
No usamos servicios de recuperación de los datos del usuario. Más adelante, al introducir el email se recuperarán el resto de datos personales mediante un servicio.
-
Por ahora gestionaremos las horas con listas desplegables con valores sólo para las horas, sin minutos.
Archivo main/solicitudes/pages/solicitudes.component.html
:
<div fxFlexAlign="center" fxLayoutAlign="center center">
<form [formGroup]="formHorario"> (1)
<h1>Crear reserva</h1>
<hr />
<div fxLayout="column wrap" fxLayoutGap="20px">
<mat-card> (2)
<mat-card-subtitle>Datos personales</mat-card-subtitle>
<div fxLayout="row" fxLayoutGap="20px">
<div fxFlex>
<mat-form-field appearance="outline" fxFill> (3)
<mat-label>Email</mat-label> (4)
<input
matInput (5)
formControlName="email" (6)
/>
</mat-form-field>
</div>
<div fxFlex>
<mat-form-field appearance="outline" fxFill>
<mat-label>Nombre</mat-label>
<input matInput formControlName="nombre" />
</mat-form-field>
</div>
<div fxFlex>
<mat-form-field appearance="outline" fxFill>
<mat-label>Unidad/Departamento/Centro</mat-label>
<input matInput formControlName="unidad" />
</mat-form-field>
</div>
</div>
<div fxLayout="row" fxLayoutGap="20px">
<div fxFlex>
<mat-form-field appearance="outline" fxFill>
<mat-label>Teléfono</mat-label>
<input matInput formControlName="telefono" />
</mat-form-field>
</div>
<div fxFlex>
<mat-form-field appearance="outline" fxFill>
<mat-label>Cargo</mat-label>
<div>
<mat-select formControlName="cargo"> (7)
<mat-option
*ngFor="let cargo of cargos"
value="{{ cargo }}"
>{{ cargo }}</mat-option
>
</mat-select>
</div>
</mat-form-field>
</div>
<div fxFlex></div>
</div>
</mat-card>
<mat-card> (8)
<mat-card-subtitle>Datos de la actividad</mat-card-subtitle>
<div fxLayout="row" fxLayoutGap="20px">
<div>
<mat-form-field appearance="outline">
<mat-label>Tipo</mat-label>
<mat-select formControlName="tipo">
<mat-option value="docente">Docente</mat-option>
<mat-option value="noDocente">No docente</mat-option>
</mat-select>
</mat-form-field>
</div>
<div fxFlex>
<mat-form-field appearance="outline" fxFill>
<mat-label>Actividad</mat-label>
<input matInput formControlName="nombreActividad" />
</mat-form-field>
</div>
</div>
</mat-card>
<mat-card>
<mat-card-subtitle>Horario de la reserva</mat-card-subtitle>
<div fxLayout="row" fxLayoutGap="20px">
<div fxFlex>
<mat-form-field appearance="fill">
<mat-label>Rango de fechas</mat-label>
<mat-date-range-input [rangePicker]="picker"> (9)
<input
matStartDate
formControlName="start"
placeholder="Start date"
/>
<input
matEndDate
formControlName="end"
placeholder="End date"
/>
</mat-date-range-input>
<mat-datepicker-toggle
matSuffix
[for]="picker"
></mat-datepicker-toggle>
<mat-date-range-picker #picker></mat-date-range-picker> (10)
</mat-form-field>
</div>
<div fxFlex>
<mat-form-field appearance="outline">
<mat-label>Día</mat-label>
<mat-select formControlName="dia"> (11)
<mat-option *ngFor="let day of daysOfWeek" value="{{ day }}">{{
day | titlecase
}}</mat-option>
</mat-select>
</mat-form-field>
</div>
<div fxFlex>
<mat-form-field appearance="outline">
<mat-label>Hora de inicio</mat-label>
<mat-select formControlName="horaInicio"> (12)
<mat-option *ngFor="let hour of hours" value="{{ hour }}">{{
hour
}}</mat-option>
</mat-select>
</mat-form-field>
</div>
<div fxFlex>
<mat-form-field appearance="outline">
<mat-label>Hora de fin</mat-label>
<mat-select formControlName="horaFin"> (13)
<mat-option *ngFor="let hour of hours" value="{{ hour }}">{{
hour
}}</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
<div fxLayout="row" fxLayoutAlign="end">
<button mat-stroked-button color="primary" (click)="save()"> (14)
Guardar
</button>
</div>
</mat-card>
</div>
</form>
</div>
1 | Objeto formulario ligado al formulario reactivo formHorario definido en la parte TypeScript |
2 | Tarjeta para elementos de datos personales |
3 | Creación de campo de formulario Material |
4 | Etiqueta |
5 | Input de tipo Material |
6 | Vinculación del campo email del formulario a su homólogo en la parte TypeScript |
7 | Listbox inicializado con los valores de ejemplo definidos en la parte TypeScript |
8 | Tarjeta para los datos de la actividad |
9 | Elemento para los datos del rango de fechas |
10 | Elemento para la selección del rango de fechas |
11 | Desplegable para la selección de días |
12 | Desplegable para la selección de la hora de inicio |
13 | Desplegable para la selección de la hora de fin |
14 | Botón con llamada al método que gestionará el formulario |
Si pulsamos el botón Crear
y no se cumple alguno de los validadores, los campos no válidos aparecerán marcados en rojo. Y si activamos en el navegador las Herramientas para desarrolladores, como el método save
hace un console.log
del objeto formHorario
, vemos que su estado es INVALID
. Esto se debe a que no se está cumpliendo alguno de sus validadores.
A continuación veremos cómo mostrar mensajes de error en las validaciones y cómo desactivar el botón del formulario hasta que éste sea válido.
4.4.1. Añadiendo mensajes de error en las validaciones
Cuando no se cumple un validador, el campo en cuestión debería mostrar alguna señal. Necesitamos por tanto métodos que nos devuelvan si los campos son válidos o no. Pero en lugar de tener varios métodos que indiquen si un campo tiene errores o no, vamos a construir un método genérico, un método al que le podamos pasar un campo como parámetro y nos indique si el campo tiene errores o no. En nuestro caso, los campos tendrán errores si no se cumplen algunos de los validadores. No obstante, para mejorar la experiencia de usuario, no queremos que se muestren mensajes de error al abrir el formulario, cuando un usuario aún no ha introducido datos, ya que aunque no se cumplirán los validadores porque los campos aún están vacíos, no conviene abrir un formulario indicando que ya se tienen errores. Para mostrar un error sobre un campo deberíamos esperar a que al menos haya sido tocado. Por tanto, en el método de comprobación de las validaciones introduciremos además la condición de que los campos hayan sido tocados para que inicialmente no se consideren erróneos los campos que aún no han sido tocados.
A continuación se muestra el método isNotValidField()
que devuelve que un campo no es válido si ha sido tocado y contiene errores. Archivo main/solicitudes/pages/solicitudes.component.ts
:
...
isNotValidField(field: string) {
return (
this.formHorario.controls[field].errors &&
this.formHorario.controls[field].touched
);
}
...
Para personalizar la presentación de los mensajes de error en los campos no válidos definimos una clase invalid-mat-form-field
en styles.css
.invalid-mat-form-field {
font-size: small;
color: red;
}
Por último, si isNotValidField
devuelve que el campo no es válido añadimos la presentación (condicional) del error en un elemento <span>
Archivo main/solicitudes/pages/solicitudes.component.html
:
...
<mat-form-field appearance="outline" fxFill>
<mat-label>Email</mat-label>
<input
matInput
formControlName="email"
/>
<span class="invalid-mat-form-field" *ngIf="isNotValidField('email')"
>* Formato de email incorrecto</span
> (1)
</mat-form-field>
...
<mat-form-field appearance="outline" fxFill>
<mat-label>Actividad</mat-label>
<input matInput formControlName="nombreActividad" />
<span
class="invalid-mat-form-field"
*ngIf="isNotValidField('nombreActividad')"
>Al menos 5 caracteres</span
> (2)
</mat-form-field>
...
1 | Presentación de mensaje de error si el email no es válido |
2 | Presentación de mensaje de error si la actividad no es válida |
A continuación se muestra el efecto de la presentación del mensaje de error cuando los campos no son válidos.
4.4.2. Desactivación condicional del botón Crear
Otra funcionalidad interesante es hacer que el botón Crear
no esté habilitado si el formulario no es válido. Dado que los formularios disponen de la propiedad valid
que indica si el formulario es válido, podemos aprovechar el valor de esta propiedad para controlar la activación del botón Crear
. Para ello, comenzaremos añadiendo un método al TypeScript del componente que indique si el formulario es válido o no basándonse en la propiedad valid
de los formularios.
El formulario es |
Archivo main/solicitudes/pages/crear/crear.component.ts
:
...
isValidForm() {
return this.formHorario.valid;
}
...
Ahora sólo falta configurar la propiedad disabled
del formulario en función de lo que devuelva el método isValidForm
.
Archivo main/solicitudes/pages/crear/crear.component.html
:
...
<button
mat-stroked-button
color="primary"
(click)="save()"
[disabled]="!isValidForm()" (1)
>
Crear
</button>
...
1 | Desactivación del botón Crear si el formulario no es válido |
Si ahora alguno de los campos no cumple sus validaciones el formulario no será válido y el botón Crear
estará desactivado.
5. Creación de un servicio de datos de persona
En esta sección crearemos un servicio que recupere datos de persona. Para ello, usaremos una API REST de prueba que contiene datos de personas, espacios y permite el almacenamiento de solicitudes. En el Anexo I. Creación de un servidor JSON para datos de prueba se describe cómo se crea esa API.
Comenzaremos creando un servicio para la recuperación de los datos de personas.
$ ng g service services/persona
Los servicios de nuestra aplicación los organizaremos en una carpeta |
HttpClientModule en app.module.ts
import { NgModule } from '@angular/core';
import { FlexLayoutModule } from '@angular/flex-layout';
import { BrowserModule } from '@angular/platform-browser';
import { HttpClientModule } from '@angular/common/http'; (1)
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { SharedModule } from './shared/shared.module';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
@NgModule({
declarations: [AppComponent],
imports: [
BrowserModule,
HttpClientModule, (2)
AppRoutingModule,
BrowserAnimationsModule,
FlexLayoutModule,
SharedModule,
],
providers: [],
bootstrap: [AppComponent],
})
export class AppModule {}
1 | Importación del módulo de HttpClientModule |
2 | Incorporación a la lista de imports |
Para implementar el servicio:
-
Inyectaremos
HttpClient
en el constructor para poder realizar peticiones HTTP. -
Inicializaremos la URL de acceso a la API.
-
Implementaremos un método que permita la recuperación de una persona por su email.
Servicio en services/people.ts
:
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
@Injectable({
providedIn: 'root',
})
export class PeopleService {
url = 'http://localhost:3000/personas'; (1)
constructor(private http: HttpClient) {} (2)
findOne(email: any): Observable<any> { (3)
return this.http.get(`${this.url}?email=${email}`);
}
}
1 | Configuracion de la ruta base |
2 | Inyección de HttpClient para realizar operaciones HTTP contra la API REST |
3 | Método que devuelve un observable con los datos de una persona a partir de su email. |
En la documentación de JSON Server se indica cómo filtrar y ordenar resultados. |
5.1. Uso de variables de entorno
En el ejemplo anterior, teníamos la URL de la API REST en el propio código de la aplicación (lo que se conoce como hardcoded). Esto presenta problemas de mantenimiento porque si cambiase la URL tendríamos que hacer cambios en todos los archivos en los que se esté usando. Pero otro detalle muy importante, es que probablemente tendremos que cambiar el valor en función de si estamos en el entorno de desarrollo o en el entorno de producción.
Angular permite la definición de archivos de variables de entorno y permite tener archivos separados para los entornos de desarrollo y producción. Los procesos de despliegue en los entornos de CI/CD tomarán los valores del archivo del entorno de producción, mientras que cuando estemos desarrollando, ng serve
toma los valores del entorno de desarrollo al ejecutar la aplicación.
Estos son los archivos de variables de entorno que manejaremos en nuestra aplicación Angular:
-
environments/environments.ts
: Variables de entorno para desarrollo. -
environments/environments.prod.ts
: Variables de entorno para producción.
A continuación se muestra el archivo de variables de entorno para desarrollo.
Archivo environments/environments.ts
:
export const environment = {
production: false,
urlPersonas: 'http://localhost:3000/personas', (1)
};
Una vez definido, podremos usar sus variables en el resto de la aplicación. Veamos cómo quedaría el servicio usando variables de entorno.
Servicio en services/people.ts
:
import { environment } from './../../environments/environment'; (1)
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
@Injectable({
providedIn: 'root',
})
export class PeopleService {
constructor(private http: HttpClient) {}
findOne(email: any): Observable<any> {
return this.http.get(`${environment.urlPersonas}?email=${email}`); (2)
}
}
1 | Importación de variables de entorno de desarrollo |
2 | Uso de las variables de entorno |
Hay que tener cuidado a la hora de importar las variables de entorno y no importar el de producción ( |
También habría que configurar las variables de entorno de producción. A continuación se muestra un ejemplo para producción en el que no se ha puesto un servidor específico para la API REST.
Archivo environments/environments.prod.ts
:
export const environment = {
production: true,
urlPersonas: 'http://<your-production-people-api-server>/personas', (1)
};
1 | Configuración para producción |
6. Mejora de la carga de datos en el formulario de creación de solicitudes
Hasta ahora, al inicializar el formulario de creación de solicitudes, los datos de la persona eran incluidos sin capacidad de ser cambiados mediante una inicialización de valores en el método ngOnInit
.
ngOnInit(): void {
this.formHorario.controls['nombre'].setValue('Manuel Torres Gil');
this.formHorario.controls['unidad'].setValue('Informática');
this.formHorario.controls['telefono'].setValue('84030');
}
Sin embargo, el funcionamiento esperado es que estos datos fuesen cargados a partir del email introducido en el formulario. Actualmente contamos con el método findOne()
en el servicio PersonaService
que permite recuperar los datos de una persona a partir de su email. Sin embargo, esto aún no está siendo explotado por la aplicación. Veamos cómo hacerlo.
En primer lugar, dejaremos el método ngOnInit()
vacío. Ahora la inicialización se delegará en un método dedicado a ello. Dicho método será llamado cada vez que se introduzca un email en el formulario.
Realmente necesitaremos dos métodos:
-
Un método
buscarPersona()
que llamará al servicio de búsqueda de personas por email. -
Un método
actualizarCamposPersona()
que será el que actualice el formulario con los datos recuperados por el método anterior. El métodoactualizarCamposPersona()
será llamado cuando se introduzca un email en el formulario.
Hacemos una prueba llamando directamente a la API REST con Postman o con un navegador recuperando la persona a partir de su email para ver la estructura de datos de la respuesta. Al hacer la petición siguiente:
obtenemos la respuesta siguiente:
Importante: Vemos que la persona es un objeto que pertenece a un array. |
Archivo main/solicitudes/pages/crear/crear.component.ts
:
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { lastValueFrom, Observable, of, shareReplay } from 'rxjs';
import { PersonaService } from '../../../../services/persona.service';
@Component({
selector: 'app-crear',
templateUrl: './crear.component.html',
styleUrls: ['./crear.component.css'],
})
export class CrearComponent implements OnInit {
cargos: string[] = []; (1)
persona: any; (2)
formHorario: FormGroup = this.fb.group({
nombre: [{ value: '', disabled: true }],
cargo: [, [Validators.required]],
unidad: [{ value: '', disabled: true }],
telefono: [{ value: '', disabled: true }],
email: [, [Validators.required, Validators.email]],
tipo: [,],
nombreActividad: [, [Validators.required, Validators.minLength(5)]],
start: [,],
end: [,],
dia: [,],
horaInicio: [,],
horaFin: [,],
});
diasSemana: string[] = [
'lunes',
'martes',
'miercoles',
'jueves',
'viernes',
'sabado',
'domingo',
];
horas = Array.from(Array(24).keys());
constructor(
private fb: FormBuilder,
private personaService: PersonaService (3)
) {}
ngOnInit(): void {} (4)
isNotValidField(field: string) {
return (
this.formHorario.controls[field].errors &&
this.formHorario.controls[field].touched
);
}
isValidForm() {
return this.formHorario.valid;
}
buscarPersona(email: any) { (5)
this.personaService.findOne(email).subscribe((res) => { (6)
this.persona = res[0]; (7)
});
}
actualizarCamposPersona() { (8)
const email = this.formHorario.controls['email'].value; (9)
this.buscarPersona(email); (10)
if (this.persona) { (11)
this.cargos = [...this.persona.cargo];
this.formHorario.controls['nombre'].setValue(this.persona.nombre);
this.formHorario.controls['unidad'].setValue(this.persona.unidad);
this.formHorario.controls['telefono'].setValue(this.persona.telefono);
this.persona.cargo = this.formHorario.controls['cargo'].value;
return;
}
}
save() {
console.log('this.formHorario :>> ', this.formHorario);
}
}
1 | Variable para almacenar los cargos recuperados de una persona. Se usa para poblar el desplegable en el formulario |
2 | Variable para almacenar la persona recuperada del servicio |
3 | Inyección del servicio en el constructor |
4 | Ahora ya no se inicializan los datos de la persona desde ngOnInit |
5 | Método para la búsqueda de una persona mediante su email |
6 | Llamada al método del servicio que recupera los datos de una persona |
7 | Almacenamiento de los datos recuperados en la variable de instancia persona . Vimos que la persona recuperada está en la primera posición del array |
8 | Método de actualización de datos en el formulario |
9 | Acceso al valor del email introducido en el formulario |
10 | Llamada al método de búsqueda de personas por email |
11 | Actualización de datos en el formulario si se recupera una persona |
Ahora ya sólo falta llamar al método actulizarCamposPersona()
desde el cuadro de texto del email de la parte HTML del componente.
Archivo main/solicitudes/pages/crear/crear.component.html
:
<div fxFlexAlign="center" fxLayoutAlign="center center">
<form [formGroup]="formHorario" autocomplete="off">
<h1>Crear reserva</h1>
<hr />
<div fxLayout="column wrap" fxLayoutGap="20px">
<mat-card>
<mat-card-subtitle>Datos personales</mat-card-subtitle>
<div fxLayout="row" fxLayoutGap="20px">
<div fxFlex>
<mat-form-field appearance="outline" fxFill>
<mat-label>Email</mat-label>
<input
matInput
formControlName="email"
(blur)="actualizarCamposPersona()" (1)
/>
<span
class="invalid-mat-form-field"
*ngIf="isNotValidField('email')"
>* Formato de email incorrecto</span
>
</mat-form-field>
</div>
...
1 | Llamada al método actualizarCamposPersona() tras perder el foco (evento blur ) |
Tras introducir un email registrado en el backend, se cargarán los datos de la persona. No obstante, vemos un comportamiento anómalo. Los datos no aparecen actualizados al retirar el foco de email por primera vez. Parece que hubiera que cambiar dos veces el foco, entrando y saliendo dos veces del email. Este comportamiento anómalo se debe a que los datos de la persona están llegando tarde y no están aún al perder el foco la primera vez, pero sí parece que ya están disponibles si se vuelve a cambiar el foco por segunda vez. Es decir, los datos están llegando entre los dos cambios de foco. A continuación veremos cómo solucionar este problema.
6.1. Actualizaciones con datos asíncronos.
Para evitar el problema de que los datos que devuelve el servicio lleguen con retraso y no estén a tiempo para presentarlos en la pantalla esperaremos a que lleguen los datos antes de proceder a su presentación en pantalla. El problema radica en que el método buscarPersona()
actualizaba tarde los datos de la persona. El código siguiente ilustra los cambios que hacemos en el código
buscarPersona(email: any) {
/*
this.personaService.findOne(email).subscribe((res) => { (1)
this.persona = res[0];
});
*/
return lastValueFrom(this.personaService.findOne(email));(2)
}
1 | Antigua llamada al servicio de búsqueda de persona por email |
2 | Ahora buscarPersona devuelve una promesa de un observable, que se consumirá con async/await . |
También habrá que cambiar la llamada a buscarPersona()
desde actualizarDatosPersona()
. Quedará así:
async actualizarCamposPersona() { (1)
const email = this.formHorario.controls['email'].value;
// this.buscarPersona(email); (2)
this.persona = (await this.buscarPersona(email))[0]; (3)
if (this.persona) {
this.cargos = [...this.persona.cargo];
this.formHorario.controls['nombre'].setValue(this.persona.nombre);
this.formHorario.controls['unidad'].setValue(this.persona.unidad);
this.formHorario.controls['telefono'].setValue(this.persona.telefono);
this.persona.cargo = this.formHorario.controls['cargo'].value;
return;
}
}
1 | Ahora el método es async porque dentro contiene un await |
2 | Antigua forma de llamada a buscarPersona() |
3 | Carga de datos en persona. Recordemos que la API devolvía la persona en un array y había que recuperar el primero. |
Ahora, la carga de datos en persona no se realiza hasta que no se hayan recuperado sus datos del servicio y se habrá corregido aquel comportamiento anómalo.
6.2. Mejora del código de actualización del formulario con patchValue
En el código anterior teníamos un código engorroso que puede ser mejorado. Se trata de:
this.formHorario.controls['nombre'].setValue(this.persona.nombre);
this.formHorario.controls['unidad'].setValue(this.persona.unidad);
this.formHorario.controls['telefono'].setValue(this.persona.telefono);
Esto podría ser aún peor si en lugar de tener que actualizar 3 campos tuviésemos que actuliazar 10.
Para ello, cuando los nombres de los controles del formulario coincidan con los nombres usados en los objetos que contienen los datos (nombre - nombre
, unidad - unidad
, telefono - telefono
) podemos usar patchValue
que actualizará todos los valores que tengan el mismo nombre.
Así, el código anterior quedaría de la siguiente forma, mucho más limpio.
async actualizarCamposPersona() {
const email = this.formHorario.controls['email'].value;
this.buscarPersona(email);
//this.persona = (await this.buscarPersona(email))[0];
if (this.persona) {
this.cargos = [...this.persona.cargo];
this.formHorario.patchValue(this.persona); (1)
this.persona.cargo = this.formHorario.controls['cargo'].value;
return;
}
this.clearPersonalData();
}
1 | patchValue hace la actualización de todos los datos en una sola línea |
6.3. Limpieza de datos cuando la persona no existe
Si probamos a introducir una persona que no existe, comprobaremos que no se actualizan los datos, lo que podría inducir a error. Si el formulario estaba vacío y se introduce un email inexistente, no se mostrarán datos, por lo que este fallo pasaría desapercibido. Pero si ya había datos y se introduce un nuevo email inexistente en la API REST, se mantendrán los datos de la persona anterior, lo que no es correcto.
La solución planteada consiste en crear un método que limpie el formulario si no se recuperan datos (persona
no contiene datos). Para mejorar la experiencia de usuario usaremos el componente Snackbar de Material, que muestra una barra al pie útil para mensajes.
El módulo |
A continuación se muestra el código completo de cómo quedaría el componente con el nuevo método de limpieza de datos cuando se introducen emails no existentes.
Archivo main/solicitudes/pages/crear/crear.component.ts
:
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { lastValueFrom } from 'rxjs';
import { PersonaService } from '../../../../services/persona.service';
import { MatSnackBar } from '@angular/material/snack-bar'; (1)
@Component({
selector: 'app-crear',
templateUrl: './crear.component.html',
styleUrls: ['./crear.component.css'],
})
export class CrearComponent implements OnInit {
cargos: string[] = [];
persona: any;
formHorario: FormGroup = this.fb.group({
nombre: [{ value: '', disabled: true }],
cargo: [, [Validators.required]],
unidad: [{ value: '', disabled: true }],
telefono: [{ value: '', disabled: true }],
email: [, [Validators.required, Validators.email]],
tipo: [,],
nombreActividad: [, [Validators.required, Validators.minLength(5)]],
start: [,],
end: [,],
dia: [,],
horaInicio: [,],
horaFin: [,],
});
diasSemana: string[] = [
'lunes',
'martes',
'miercoles',
'jueves',
'viernes',
'sabado',
'domingo',
];
horas = Array.from(Array(24).keys());
constructor(
private fb: FormBuilder,
private personaService: PersonaService,
private snackBar: MatSnackBar (2)
) {}
ngOnInit(): void {}
isNotValidField(field: string) {
return (
this.formHorario.controls[field].errors &&
this.formHorario.controls[field].touched
);
}
isValidForm() {
return this.formHorario.valid;
}
buscarPersona(email: any) {
return lastValueFrom(this.personaService.findOne(email));
}
async actualizarCamposPersona() {
const email = this.formHorario.controls['email'].value;
this.persona = (await this.buscarPersona(email))[0];
if (this.persona) {
this.cargos = [...this.persona.cargo];
this.formHorario.patchValue(this.persona);
this.persona.cargo = this.formHorario.controls['cargo'].value;
return;
}
this.clearPersonalData(); (3)
}
clearPersonalData() { (4)
this.formHorario.reset(); (5)
this.snackBar.open('Persona no disponible', '', { (6)
duration: 1500,
});
}
save() {
console.log('this.formHorario :>> ', this.formHorario);
}
}
1 | Importación del componente MatSnackBar |
2 | Inyección del componente MatSnakcBar para poder usarlo en el compomente. |
3 | Llamada al método de limpieza si persona no tiene datos |
4 | Método de limpieza del formulario |
5 | Limpieza de los datos del formulario |
6 | Presentación del mensaje de error en la snackbar durante 1500 ms (1.5 segudos) |
A continuación se muestra el efecto de borrado de los datos del formulario y la presentación del mensaje de error en la barra tras introducir un email que no existe.
7. Guardado de datos en backend
En esta sección veremos cómo guardar los datos en el backend. Básicamente tendremos que
-
Crear el servicio que se encargará del almacenamiento en el backend.
-
Actualizar el método
save()
para que llame al servicio de almacenamiento anterior.
7.1. Creación del servicio de almacenamiento
Comenzamos creando un nuevo servicio para las solicitudes
$ ng g service services/solicitudes
7.2. Modificación de las variables de entorno
Hasta ahora tenemos una URL desde donde recuperamos los datos de las personas. Este servicio realmente podría ser ajeno al de la aplicación de espacios de este tutorial. Lo normal es que nuestra aplicación de espacios cuente con servicios para gestión de solicitudes y consulta de espacios. Todos ellos los vamos a incluir en la misma URL y posiblemente será diferente de la URL de la API de personas que, como hemos comentado, es algo externo a esta aplicación de espacios. Por tanto, tendremos variables de entorno diferentes.
Archivo environments/environments.ts
:
export const environment = {
production: false,
urlPersonas: 'http://localhost:3000/personas',
urlEspacios: 'http://localhost:3000',
};
La API de espacios tendrá endpoints como los siguientes. Todos ellos, tienen como elemento común urlEspacios
.
Archivo environments/environments.prod.ts
:
export const environment = {
production: true,
urlPersonas: 'http://<your-production-people-api-server>/personas',
urlEspacios: 'http://<your-production-espacios-api-server>',
};
El método de almacenamiento en el servicio en el archivo services/reservations.service.ts
sería algo así:
import { environment } from './../../environments/environment';
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root',
})
export class ReservationsService {
constructor(private http: HttpClient) {}
save(data: any): Observable<any> { (1)
return this.http.post(`${environment.urlEspacios}/reservations`, data);
}
}
1 | Método que almacena los datos y devuelve un observable |
A continuación modificaremos el método save()
del componente de crear solicitudes para que llame al servicio anterior. Además, para ofrecer una mejor experiencia de usuario, mostraremos un mensaje en la snackbar indicando que se ha creado la solicitud y redirigiremos al usuario a la pantalla del listado de solicitudes. Allí podrá ver su solicitud creada, aunque aún no podrá ver nada ya que no está implementada. En la sección siguiente implementaremos la funcionalidad de mostrar el listado de solicitudes.
Archivo main/solicitudes/pages/crear/crear.component.ts
:
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { lastValueFrom } from 'rxjs';
import { PersonaService } from '../../../../services/persona.service';
import { MatSnackBar } from '@angular/material/snack-bar';
import { SolicitudesService } from '../../../../services/solicitudes.service';
import { Router } from '@angular/router';
@Component({
selector: 'app-crear',
templateUrl: './crear.component.html',
styleUrls: ['./crear.component.css'],
})
export class CrearComponent implements OnInit {
cargos: string[] = [];
persona: any;
formHorario: FormGroup = this.fb.group({
nombre: [{ value: '', disabled: true }],
cargo: [, [Validators.required]],
unidad: [{ value: '', disabled: true }],
telefono: [{ value: '', disabled: true }],
email: [, [Validators.required, Validators.email]],
tipo: [,],
nombreActividad: [, [Validators.required, Validators.minLength(5)]],
start: [,],
end: [,],
dia: [,],
horaInicio: [,],
horaFin: [,],
});
diasSemana: string[] = [
'lunes',
'martes',
'miercoles',
'jueves',
'viernes',
'sabado',
'domingo',
];
horas = Array.from(Array(24).keys());
constructor(
private fb: FormBuilder,
private personaService: PersonaService,
private solicitudesService: SolicitudesService, (1)
private snackBar: MatSnackBar,
private router: Router (2)
) {}
ngOnInit(): void {}
isNotValidField(field: string) {
return (
this.formHorario.controls[field].errors &&
this.formHorario.controls[field].touched
);
}
isValidForm() {
return this.formHorario.valid;
}
buscarPersona(email: any) {
return lastValueFrom(this.personaService.findOne(email));
}
async actualizarCamposPersona() {
const email = this.formHorario.controls['email'].value;
this.persona = (await this.buscarPersona(email))[0];
if (this.persona) {
this.cargos = [...this.persona.cargo];
this.formHorario.patchValue(this.persona);
this.persona.cargo = this.formHorario.controls['cargo'].value;
return;
}
this.clearPersonalData();
}
clearPersonalData() {
this.formHorario.reset();
this.snackBar.open('Persona no disponible', '', {
duration: 1500,
});
}
save() {
let solicitud = this.formHorario.getRawValue(); (3)
this.solicitudesService.save(solicitud).subscribe((res) => { (4)
if (res) { (5)
this.snackBar.open('Solicitud creada', '', { (6)
duration: 1500,
});
this.router.navigate(['/solicitudes/consultar']); (7)
}
});
}
}
1 | Inyección del servicio de gestión de solicitudes |
2 | Inyección de Router para poder ir a la página del listado de solicitudes tras la creación de una solicitud |
3 | Inicializar un objeto solicitud con todos los valores introducidos en el formulario |
4 | Llamada al método save del servicio pasándole los datos de la solicitud a crear |
5 | Comprobación de almacenamiento correcto |
6 | Presentación de la snackbar con el mensaje de solicitud creada |
7 | Redirigir a la página de listado de solicitudes |
8. Lista de solicitudes
En esta sección crearemos la página que muestra las solicitudes creadas. Inicialmente las mostrará todas. Después se podría añadir la posibilidad de filtrado para la consulta de solicitudes realizadas.
8.1. Servicio de lista de solicitudes
Comenzaremos añadiendo al servicio Solicitudes
un método que recupere todas las solicitudes.
import { Observable } from 'rxjs';
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { environment } from 'src/environments/environment';
@Injectable({
providedIn: 'root',
})
export class SolicitudesService {
constructor(private http: HttpClient) {}
findAll(): Observable<any> { (1)
return this.http.get(`${environment.urlEspacios}/solicitudes`); (2)
}
save(data: any): Observable<any> {
return this.http.post(`${environment.urlEspacios}/solicitudes`, data);
}
}
1 | Método para recuperar las solicitudes. Devuelve un observable |
2 | Llamada al endpoint que recupera las solicitudes |
8.2. Carga de datos en el componente de listado de solicitudes
Para la presentación de datos usaremos el componente Table
de Angular Material, que ya tenemos incluido en nuestro módulo Material
. Este componente tiene una parte TypeScript y una parte HTML. En la parte TypeScript básicamente debemos inicializar el conjunto de datos a mostrar y la lista de columnas a mostrar. Como peculiaridad, indicar que hay que cargar de forma independiente cada columna de datos. Ea decir, la tabla se carga por columnas, no por filas. Veamos cómo hacerlo:
Archivo main/solicitudes/pages/consultar/consultar.component.ts
:
import { Component, OnInit } from '@angular/core';
import { SolicitudesService } from '../../../../services/solicitudes.service';
@Component({
selector: 'app-consultar',
templateUrl: './consultar.component.html',
styleUrls: ['./consultar.component.css'],
})
export class ConsultarComponent implements OnInit {
dataSource: any = [] (1)
displayedColumns= ['nombre', 'cargo', 'unidad', 'telefono']; (2)
constructor(
private solicitudesService: SolicitudesService, (3)
private snackBar: MatSnackBar (4)
) {}
ngOnInit(): void { (5)
this.solicitudesService.findAll().subscribe((res) => { (6)
this.dataSource = res; (7)
if (this.dataSource.length == 0) { (8)
this.snackBar.open('No hay solicitudes', '', {
duration: 1500,
});
}
});
}
}
1 | Variable para almacenar los datos recuperados por el servicio |
2 | Variable para indicar las columnas a mostrar |
3 | Inyección del servicio de solicitudes |
4 | Inyección de la snackbar para presentar mensajes al pie |
5 | Inicializar la tabla al iniciar el componente |
6 | Suscripción al método que recupera las solicitudes |
7 | Almacenar los datos recuperados del servicio |
8 | Mostrar mensaje de error si no hay datos |
A continuación vamos con la parte de la presentación de los datos (el código está copiado tal cual de la documentación cambiando los nombres de campo)
<div fxLayout="column" fxLayoutAlign="center center">
<h1>Listado de solicitudes</h1>
<div *ngIf="dataSource.length > 0"> (1)
<hr />
<mat-card>
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8"> (2)
<ng-container matColumnDef="nombre"> (3)
<th mat-header-cell *matHeaderCellDef>Nombre</th> (4)
<td mat-cell *matCellDef="let element">{{ element.nombre }}</td> (5)
</ng-container>
<ng-container matColumnDef="cargo">
<th mat-header-cell *matHeaderCellDef>Cargo</th>
<td mat-cell *matCellDef="let element">{{ element.cargo }}</td>
</ng-container>
<ng-container matColumnDef="unidad">
<th mat-header-cell *matHeaderCellDef>Unidad</th>
<td mat-cell *matCellDef="let element">{{ element.unidad }}</td>
</ng-container>
<ng-container matColumnDef="telefono">
<th mat-header-cell *matHeaderCellDef>Teléfono</th>
<td mat-cell *matCellDef="let element">{{ element.telefono }}</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr> (6)
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr> (7)
</table>
</mat-card>
</div>
</div>
1 | Presentación de la tabla si contiene datos |
2 | Inicialización de la tabla con su fuente de datos definida en la parte TypeScript |
3 | Definición de la columna del nombre |
4 | Etiqueta que se quiere presentar en esta columna |
5 | Indicar el campo del que se recuperarán los datos |
6 | Crear la fila de cabecera |
7 | Creación del cuerpo de la tabla |
A continuación se muestra el listado de resultados.
9. Lista de espacios
De forma análoga a como acabamos de hacer con el listado de solicitudes vamos a implementar la parte del listado de espacios, el cual guarda bastante parecido con el anterior. Comenzaremos creando un listado total y posteriormente le añadiremos capacidades de filtrado.
La forma de proceder será la de siempre:
-
Construcción del servicio que interactúa con el backend.
-
Programación de la parte TypeScript del componente para que interactúe con el servicio anterior.
-
Creación de la parte HTML de presentación del componente conectada a su parte TypeScript creada en el paso anterior.
9.1. Generación del servicio
En esta sección crearemos un servicio que recupere datos de espacios. Comenzaremos creando un servicio.
$ ng g service services/espacios
A continuación le añadiremos un método que recupere todos los espacios programando un método para ello en el servicio services/espacios.ts
:
import { Observable } from 'rxjs';
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { environment } from 'src/environments/environment';
@Injectable({
providedIn: 'root',
})
export class EspaciosService {
constructor(private http: HttpClient) {} (1)
findAll(): Observable<any> { (2)
return this.http.get(`${environment.urlEspacios}/espacios`); (3)
}
}
1 | Inyección del cliente HTTP |
2 | Método que devuelve los espacios como un observable |
3 | Llamada al endpoint usando la variable de entorno |
La variable de entorno se reutiliza. Ya la usamos en el servicio de solicitudes. Sólo necesitamos la URL del servidor. El resto de la ruta se añade en cada módulo funcional de la API (p.e. |
Este servicio atacará a la API REST de prueba que estamos usando en este tutorial y que está disponible en Anexo I. Creación de un servidor JSON para datos de prueba.
9.2. Programación del componente
Una vez creado el servicio, ya podemos programar el componente TypeScript que lo usará para recuperar datos de espacios y entregarlos a la parte HTML del componente. Lo haremos en el archivo main/espacios/pages/consultar/consultar.component.ts
:
import { Component, OnInit } from '@angular/core';
import { EspaciosService } from '../../../../services/espacios.service';
@Component({
selector: 'app-consultar',
templateUrl: './consultar.component.html',
styleUrls: ['./consultar.component.css'],
})
export class ConsultarComponent implements OnInit {
espacios: any = []; (1)
constructor(private espaciosService: EspaciosService) {} (2)
ngOnInit(): void { (3)
this.espaciosService.findAll().subscribe((res) => { (4)
this.espacios = res; (5)
});
}
}
1 | Variable donde volcaremos los datos de los espacios recuperados por el servicio |
2 | Inyección del servicio de espacios |
3 | Recuperación de los espacios al iniciar el componente |
4 | Llamada al método del servicio que recupera los espacios |
5 | Carga de los datos leídos en la variable de espacios |
9.3. Configuración de las importaciones del módulo
Como el componente de presentación de espacios usará componentes Material, hay que asegurarse que nuestro módulo de componentes Material está incluido en el módulo del componente de espacios, que es con el estamos trabajando para la lista de espacios. A continuación se muestra la configuración de las importaciones de main/espacios/espacios.module.ts
para que esté incluido nuestro módulo de Material
:
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { EspaciosRoutingModule } from './espacios-routing.module';
import { ConsultarComponent } from './pages/consultar/consultar.component';
import { MaterialModule } from '../../material/material.module';
@NgModule({
declarations: [ConsultarComponent],
imports: [CommonModule, EspaciosRoutingModule, MaterialModule], (1)
})
export class EspaciosModule {}
1 | Módulo de Material |
9.4. Programación de la presentación de datos
En esta sección vamos a ver cómo presentamos los datos en el componente de espacios. Básicamente se trata de explotar la variable espacios
inicializada en la parte TypeScript del componente y que es la que contiene los datos. Para ello, se iterará sobre espacios
y se mostrarán los datos en tarjetas Material. Dentro de cada espacio, puede haber una lista de reservas, tal y como se muestra a continuación. Por tanto, habrá que crear un bucle anidado que itere sobre las reservas de un espacio.
{ (1)
"edificio": "Aulario II",
"aula": "Aula 1",
"fecha": "07/02/2022",
"reservas": [ (2)
{
"hora": "09:00 - 12:00",
"descripcion": "GRUPO A",
"asignatura": "( 31103209 ) - Lexicología y Semántica Inglesas",
"profesor": ""
},
{
"hora": "12:00 - 15:00",
"descripcion": "GRUPO A",
"asignatura": "( 12104226 ) - Historia de la Lengua Española II",
"profesor": ""
},
{
"hora": "16:00 - 19:00",
"descripcion": "GRUPO UNICO",
"asignatura": "( 40153329 ) - Teoría de Códigos y Criptografía",
"profesor": ""
}
]
}
1 | Datos correspondientes a la ocupación de un espacio en un día concreto |
2 | Lista de reservas realizadas en un espacio en un día concreto |
Consideramos que la API REST devuelve ocupaciones diarias de un espacio. Cada día en un espacio es considerado de forma independiente a otro día en el mismo espacio. En la parte de |
Archivo main/espacios/pages/consultar/consultar.component.html
:
<div
*ngIf="espacios.length " (1)
fxLayout="column"
fxLayoutAlign="center "
fxLayoutGap="10px"
>
<mat-card *ngFor="let espacio of espacios" class="mt-3" fxFill> (2)
<mat-card-subtitle>
<mat-label class="mat-body-strong">Edificio: </mat-label
>{{ espacio.edificio }} (3)
<mat-label class="mat-body-strong">Aula: </mat-label> {{ espacio.aula }}
<mat-label class="mat-body-strong">Fecha: </mat-label>{{ espacio.fecha }}
</mat-card-subtitle>
<mat-list>
<mat-list-item *ngFor="let reserva of espacio.reservas"> (4)
<mat-card-content>
<mat-label class="mat-body-strong">Hora: </mat-label
>{{ reserva.hora }} (5)
<mat-label class="mat-body-strong">Descripción: </mat-label
>{{ reserva.descripcion }}
<mat-label class="mat-body-strong">Asignatura: </mat-label
>{{ reserva.asignatura }}
<mat-label class="mat-body-strong">Profesor: </mat-label
>{{ reserva.profesor }}
</mat-card-content>
</mat-list-item>
</mat-list>
</mat-card>
</div>
1 | Impide que haya errores en el *ngFor siguiente cuando no hay datos |
2 | Bucle para recorrer los espacios |
3 | Presentación de datos de espacios |
4 | Bucle anidado para recorrer las reservas de un espacio |
5 | Presentación de los datos de una reserva |
Aquí se muestra el resultado. Por ahora se muestran todas las solicitudes. En el apartado siguiente veremos cómo se le pueden incorporar funcionalidades de filtrado.
9.5. Incorporación de la funcionalidad de filtrado
En esta sección vamos a ver cómo llevar el código anterior del listado de solicitudes a un componente específico. Haremos esto porque vamos a estructurar la página de espacios en dos componentes. Uno para un componente de filtrado con cuadros de lista y de entrada de datos para especificar condiciones de filtrado. Otro para el componente de presentación de la tabla de datos. La figura siguiente ilustra un esbozo de la página mostrando la funcionalidad de filtrado de espacios.
Comencemos generando el componente de la tabla de espacios para resultados de búsqueda y el del formulario de búsqueda.
$ ng g component main/espacios/components/tabla-espacios
$ ng g component main/espacios/components/form-buscar
Es habitual colocar los componentes de un módulo funcional en una carpeta Al final, deberemos tener una estructura similar a la de esta figura. |
9.5.1. Flujo de datos entre los componentes
Los componentes individuales (formulario y tabla de resultados) y el componente que los englobla tienen una forma de comunicarse y compartir datos. En la figura siguiente vemos la siguiente organización y flujo de datos entre componentes:
-
El componente padre de la página de espacios incluye los selectores de sus componentes: en este caso el de filtrado y el de tabla de datos. (pasos 1 y 2).
-
El componente del formulario de filtrado define un evento
onSearchEspacio
, que mediante el decorador@Output()
permite enviar datos al componente padre. El componente padre recibe el evento comoonSearchEspacio
(paso 3). (El objeto que contiene los datos de los espacios está en el componente de espacios, digamos el componente padre.) -
Se configura
filtroBusqueda
para que sean los datos que se envíen al padre cuando se produczca el eventoonSearchEspacio
(pasos 4 y 6). -
Se define y se implementa en el componente y el template del padre el método asociado a gestionar el evento
onSearchEspacio
(paso 5). -
Se inicializa el valor de
espacios
en el componente padre mediante la llamada al servicio de búsqueda pasándole los parámetros de filtrado (paso 7). -
El componente padre pasa al componente hijo de tabla de datos los datos de los espacios. Lo hace mediante la técnica de ligado de propiedades en el selector de inclusión al hijo (paso 8). El componente hijo de tabla de datos recibe en el TypeScript los datos de los espacios mediante un decorador
@Input()
La figura ilustra los componentes, el flujo y la parte relevante del código para conseguirlo.
9.5.2. Programación de la parte TypeScript del componente de tabla de datos
Este componente se dedica a gestionar los datos de la tabla de datos de los espacios recuperados por el servicio. Como los datos de los espacios están en el componente padre, el componente de tabla de datos tendrá que añadir el decorador @Input
y asociarle una variable, que será la que usen otros componentes para inyectarle datos.
Archivo main/espacios/component/tabla-espacios/tabla-espacios.component.ts
:
import { Component, Input, OnInit } from '@angular/core';
@Component({
selector: 'app-tabla-espacios', (1)
templateUrl: './tabla-espacios.component.html',
styleUrls: ['./tabla-espacios.component.css'],
})
export class TablaEspaciosComponent implements OnInit {
@Input() espacios: any; (2)
constructor() {}
ngOnInit(): void {}
}
1 | Selector para usar este componente |
2 | Decorador Input() que define una variable espacios en la que se recibirán los espacios |
9.5.3. Refactorización en un componente dedicado a la tabla de datos
Ahora llevaremos el código que creamos para el HTML del listado de espacios hasta el HTML de la tabla de datos, tal cual, respetando el uso de espacios
. Lo podemos llevar tal cual porque la presentación no va a variar y los datos los inyectaremos desde el padre al componete hijo a través de su @Input
para espacios
.
Archivo main/espacios/component/tabla-espacios/tabla-espacios.component.html
:
<div *ngIf="espacios.length">
<div fxLayout="column" fxLayoutAlign="center " fxLayoutGap="10px">
<mat-card *ngFor="let espacio of espacios" class="mt-3" fxFill>
<mat-card-subtitle>
<mat-label class="mat-body-strong">Edificio: </mat-label
>{{ espacio.edificio }}
<mat-label class="mat-body-strong">Aula: </mat-label>
{{ espacio.aula }} <mat-label class="mat-body-strong">Fecha: </mat-label
>{{ espacio.fecha }}
</mat-card-subtitle>
<mat-list>
<mat-list-item *ngFor="let reserva of espacio.reservas">
<mat-card-content>
<mat-label class="mat-body-strong">Hora: </mat-label
>{{ reserva.hora }}
<mat-label class="mat-body-strong">Descripción: </mat-label
>{{ reserva.descripcion }}
<mat-label class="mat-body-strong">Asignatura: </mat-label
>{{ reserva.asignatura }}
<mat-label class="mat-body-strong">Profesor: </mat-label
>{{ reserva.profesor }}
</mat-card-content>
</mat-list-item>
</mat-list>
</mat-card>
</div>
</div>
9.5.4. Configuración de la página de listado de espacios
Ahora la página de listado de espacios, que antes mostraba directamente el listado de espacios, se convertirá en una página contenedor o en una página padre que incluye dos ( componente de formulario de búsqueda y componente de filtrado de datos), tal y como se muestra a continuación.
<div fxLayout="column" fxLayoutAlign="center center" fxLayoutGap="20px">
<div fxFlex>
<h1>Lista de espacios</h1>
<app-form-buscar ></app-form-buscar> (1)
</div>
<div fxLayoutAlign="center stretch">
<app-tabla-espacios [espacios]="espacios"></app-tabla-espacios> (2)
</div>
</div>
1 | Selector para incluir el componente del formulario de búsqueda |
2 | Selector para incluir el componente de tabla de datos. Se le pasa al decorador @Input definido como espacios el valor que tenemos almacenado en espacios de este componente, el componente padre. |
Al pasar datos a un componente a través de @Input
, la sintaxis
<app-tabla-espacios [espacios]="espacios"></app-tabla-espacios>
se interpreta así: la primera parte es el nombre del Input
en el componente de destino; la segunda parte es el valor que le pasamos.
La página se verá así. Vemos que aún no aparece el formulario para el filtrado. Lo crearemos en breve. Antes, hay que adaptar el módulo de estos componentes de espacios para incluir formularios reactivos. Pero ya hemos conseguido segregar la tabla de datos a un componente hijo.
9.5.5. Modificación del módulo de espacios
Hay que hace una modificación ligera al módulo de espacios para importar el módulo de formularios reactivos. Lo va a necesitar el componente del formulario de filtrado de espacios, que lo crearemos como formulario reactivo.
import { ReactiveFormsModule } from '@angular/forms';
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { EspaciosRoutingModule } from './espacios-routing.module';
import { ConsultarComponent } from './pages/consultar/consultar.component';
import { MaterialModule } from '../../material/material.module';
import { TablaEspaciosComponent } from './components/tabla-espacios/tabla-espacios.component';
import { FormBuscarComponent } from './components/form-buscar/form-buscar.component';
@NgModule({
declarations: [
ConsultarComponent,
TablaEspaciosComponent,
FormBuscarComponent,
],
imports: [
CommonModule,
EspaciosRoutingModule,
MaterialModule,
ReactiveFormsModule, (1)
],
})
export class EspaciosModule {}
1 | Incorporación del módulo de formularios reactivos |
9.5.6. Adaptación del servicio de búsqueda para que acepte parámetros de filtrado
En el servicio de espacios, el método findAll()
actualmente devuelve todos los espacios y no ofrece posibilidad de filtrado. A continuación se muestra una modificación para que permita parámetros de filtrado. Dichos parámetros serán los que recojan los datos del formulario de filtrado.
El filtro de búsqueda llega como un objeto JSON clave-valor, donde la clave es el nombre del campo a filtrar y el valor, el valor de filtrado. La idea es ir concatenando en las peticiones los parámetros de filtrado uno detrás de otro, como se muestra a continuación. Nótese que también se ordena para que aparezcan primero los espacios con las solicitudes ordenadas en el tiempo.
http://localhost:3002/espacios?_sort=fecha&fecha=07/02/2022&aula=Aula%201
En la URL anterior |
import { Observable } from 'rxjs';
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { environment } from 'src/environments/environment';
@Injectable({
providedIn: 'root',
})
export class EspaciosService {
constructor(private http: HttpClient) {}
findAll(filtroBusqueda: any): Observable<any> {
let filtroRequest = ''; (1)
if (filtroBusqueda.start) { (2)
filtroRequest = filtroRequest.concat(`&start=${filtroBusqueda.start}`);
}
if (filtroBusqueda.end) {
filtroRequest = filtroRequest.concat(`&end=${filtroBusqueda.end}`);
}
if (filtroBusqueda.horaInicio) {
filtroRequest = filtroRequest.concat(
`&horaInicio=${filtroBusqueda.horaInicio}`
);
}
if (filtroBusqueda.horaFin) {
filtroRequest = filtroRequest.concat(
`&horaFin=${filtroBusqueda.horaFin}`
);
}
if (filtroBusqueda.edificio) {
filtroRequest = filtroRequest.concat(
`&edificio=${filtroBusqueda.edificio}`
);
}
if (filtroBusqueda.aula) {
filtroRequest = filtroRequest.concat(`&aula=${filtroBusqueda.aula}`);
}
return this.http.get(
`${environment.urlEspacios}/espacios?_sort=fecha${filtroRequest}`
);
}
}
1 | Inicialización de la variable que almacena el filtro |
2 | Pipeline que va añadiendo filtros de búsqueda al filtro |
9.5.7. Creación de un formulario inicial de búsqueda
Se trata de programar el formulario reactivo del componente de búsqueda, tanto en su parte TypeScript como en su parte HTML.
Comenzaremos con un formulario básico que aún no implementará la funcionalidad de búsqueda. Sí tendrá ya un método buscar()
, pero su cuerpo aún estará vacío. Iremos construyendo el formulario de búsqueda de forma incremental. La figura siguiente ilustra un mock del formulario a construir.
Archivo main/espacios/componentes/form-buscar/form-buscar.ts
:
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';
@Component({
selector: 'app-form-buscar',
templateUrl: './form-buscar.component.html',
styleUrls: ['./form-buscar.component.css'],
})
export class FormBuscarComponent implements OnInit {
formBuscarEspacio: FormGroup = this.fb.group({ (1)
start: [,],
end: [,],
horaInicio: [,],
horaFin: [,],
edificio: [,],
aula: [,],
busqueda: [,],
});
hours = Array.from(Array(24).keys()); (2)
constructor(private fb: FormBuilder) {} (3)
ngOnInit(): void {}
buscar() {} (4)
}
1 | Componentes del formulario reactivo |
2 | Variable para almacenar las horas con las que poblar el desplegable de horas |
3 | Inyección de FormBuilder paa usar formularios reactivos |
4 | Declaración del método buscar() que implementaremos más adelante |
A continuación crearemos la parte de presentación del formulario de búsqueda de acuerdo con el mock anterior:
Archivo main/espacios/componentes/form-buscar/form-buscar.html
:
<form [formGroup]="formBuscarEspacio"> (1)
<mat-card>
<mat-card-subtitle>Datos para la consulta</mat-card-subtitle>
<div fxLayout="row" fxLayoutGap="20px">
<div fxFlex>
<mat-form-field appearance="fill" fxFill>
<mat-label>Rango de fechas</mat-label>
<mat-date-range-input [rangePicker]="picker">
<input
matStartDate
formControlName="start" (2)
placeholder="Start date"
/>
<input matEndDate formControlName="end" placeholder="End date" />
</mat-date-range-input>
<mat-datepicker-toggle
matSuffix
[for]="picker"
></mat-datepicker-toggle>
<mat-date-range-picker #picker></mat-date-range-picker>
</mat-form-field>
</div>
<div fxFlex>
<mat-form-field appearance="outline" fxFill>
<mat-label>Hora de inicio</mat-label>
<mat-select formControlName="horaInicio"> (3)
<mat-option *ngFor="let hour of hours" value="{{ hour }}">{{
hour
}}</mat-option>
</mat-select>
</mat-form-field>
</div>
<div fxFlex>
<mat-form-field appearance="outline" fxFill>
<mat-label>Hora de fin</mat-label>
<mat-select formControlName="horaFin"> (4)
<mat-option *ngFor="let hour of hours" value="{{ hour }}">{{
hour
}}</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
<div fxLayout="row" fxLayoutGap="20px">
<div fxFlex>
<mat-form-field appearance="outline" fxFill>
<mat-label>Edificio</mat-label>
<mat-select formControlName="edificio"> (5)
<mat-option value="Aulario I">Aulario I</mat-option>
<mat-option value="Aulario II">Aulario II</mat-option>
</mat-select>
</mat-form-field>
</div>
<div fxFlex>
<mat-form-field appearance="outline" fxFill>
<mat-label>Aula</mat-label>
<mat-select formControlName="aula"> (6)
<mat-option value="Aula 1">Aula 1</mat-option>
<mat-option value="Aula 2">Aula 2</mat-option>
</mat-select>
</mat-form-field>
</div>
<div fxFlex>
<mat-form-field appearance="outline" fxFill>
<mat-label>Texto de búsqueda</mat-label>
<input matInput formControlName="busqueda" /> (7)
</mat-form-field>
</div>
</div>
<div fxLayout="row" fxLayoutAlign="center center">
<button
mat-button
mat-flat-button
color="primary"
type="submit"
(click)="buscar()" (8)
>
Buscar
</button>
</div>
</mat-card>
</form>
1 | Ligado del formulario al objeto formulario en la parte TypeScript |
2 | Ligado del input en modo de formulario reactivo |
3 | Ligado del select en modo de formulario reactivo |
4 | Ligado del select en modo de formulario reactivo |
5 | Ligado del select en modo de formulario reactivo |
6 | Ligado del select en modo de formulario reactivo |
7 | Ligado del input en modo de formulario reactivo |
8 | Ligado del input en modo de formulario reactivo |
Una vez creadas las partes TypeScript y HTML del componente de formulario, sólo falta añadirlo como componente hijo al componente de consulta de espacios.
Archivo main/espacios/pages/consultar/consultar.component.html
:
<div fxLayout="column" fxLayoutAlign="center center" fxLayoutGap="20px">
<div fxFlex>
<h1>Lista de espacios</h1>
<app-form-buscar></app-form-buscar> (1)
</div>
<div fxLayoutAlign="center stretch">
<app-tabla-espacios [espacios]="espacios"></app-tabla-espacios>
</div>
</div>
1 | Incorporación del formulario de búsqueda como componente |
Si ahora vamos a la página de consulta de espacios veremos que ya sí aparece el formulario. Nuestra página de consulta de espacios está formada por el componente de formulario y el de tabla de resultados, tal y como indicaba el mock. Sin embargo, si seleccionamos Aulario II
y Aula 1
como criterios de búsqueda y pulsamos Buscar
vemos que no se filtran los resultados.
Esto se debe a que aún no se están recuperando los parámetros de filtrado del formulario ni se están pasando al servicio de búsqueda para que devuelva un resultado filtrado. Ews decir, el componente de formulario está creado e incluido en su componente padre, pero ni se recogen sus datos de filtro de búsqueda y por tanto no se hace nada con ellos para restringir la tabla de resultados. A continuación veremos cómo solucionar este problema.
9.5.8. Incorporación de capacidades de búsqueda/filtrado
Para finalizar con el formulario de búsqueda tenemos varias tareas pendientes:
-
Modificar el TypeScript del componente hijo (el formulario de búsqueda) para implementar la función de configuración del filtro de búsqueda y para enviar los datos al componente padre (la página de consulta de espacios).
-
Implementar la función de búsqueda con parámetros en el componente padre (la página de espacios).
-
Recoger en la página de espacios los datos enviados desde el componente formulario.
Configuración del filtro de búsqueda en el componente hijo
Los componentes Angular envían sus datos a otros componentes (en este caso el formulario de búsqueda envía a la página de espacios) generando un evento personalizado a través del cual publican sus datos. En este caso los datos que publicaremos serán los valores del filtro de búsqueda recogidos mediante el formulario. Veamos cómo hacerlo.
Archivo main/espacios/components/form-buscar/form-buscar.component.ts
:
import { Component, EventEmitter, OnInit, Output } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';
@Component({
selector: 'app-form-buscar',
templateUrl: './form-buscar.component.html',
styleUrls: ['./form-buscar.component.css'],
})
export class FormBuscarComponent implements OnInit {
@Output() onSearchEspacio = new EventEmitter(); (1)
filtroBusqueda: any; (2)
formBuscarEspacio: FormGroup = this.fb.group({
start: [,],
end: [,],
horaInicio: [,],
horaFin: [,],
edificio: [,],
aula: [,],
busqueda: [,],
});
hours = Array.from(Array(24).keys());
constructor(private fb: FormBuilder) {}
ngOnInit(): void {}
buscar() {
this.filtroBusqueda = { (3)
start: this.formBuscarEspacio.controls['start'].value,
end: this.formBuscarEspacio.controls['end'].value,
horaInicio: this.formBuscarEspacio.controls['horaInicio'].value,
horaFin: this.formBuscarEspacio.controls['horaFin'].value,
edificio: this.formBuscarEspacio.controls['edificio'].value,
aula: this.formBuscarEspacio.controls['aula'].value,
};
this.onSearchEspacio.emit(this.filtroBusqueda); (4)
}
}
1 | Creación de un evento personalizado para publicar los datos del filtro de búsqueda |
2 | Objeto JSON donde almacenaremos las condiciones del filtro de búsqueda |
3 | Inicialización del filtro de búsqueda con los valores recogidos en el formulario. |
4 | Disparo del evento personalizado añadiéndole (publicando) los datos del filtro de búsqueda |
Implementacion de la función de búsqueda con parámetros en el componente padre
Se trata de incorporar al método buscar()
los parámetros de búsqueda que serán utilizados en el método findAll()
del servicio de espacios. También se usará una snackbar para mostrar posibles mensajes de error.
Archivo main/espacios/pages/consultar/consultar.component.ts
:
import { Component, OnInit } from '@angular/core';
import { EspaciosService } from '../../../../services/espacios.service';
import { MatSnackBar } from '@angular/material/snack-bar';
@Component({
selector: 'app-consultar',
templateUrl: './consultar.component.html',
styleUrls: ['./consultar.component.css'],
})
export class ConsultarComponent implements OnInit {
espacios: any = [];
constructor(
private espaciosService: EspaciosService,
private snackBar: MatSnackBar (1)
) {}
ngOnInit(): void {} (2)
buscar(filtroBusqueda: any) {
this.espacios = this.espaciosService
.findAll(filtroBusqueda) (3)
.subscribe((res) => {
this.espacios = res;
if (this.espacios.length == 0) { (4)
this.snackBar.open('No hay datos', '', {
duration: 1500,
});
}
});
}
}
1 | Inyección de la snackbar |
2 | El método de inicialización ya no muestra todas los datos de espacios ocupados |
3 | Llamada al método findAll() del servicio pasándole el filtro de búsqueda |
4 | Presentación de mensaje de error si no hay datos |
Recogida de datos en el componente padre
Aquí se trata de escuchar al evento que está publicando el componente del formulario de búsqueda y disparar una acción, que consisitirá en la llamada al método de búsqueda.
Archivo main/espacios/pages/consultar/consultar.component.html
:
<div fxLayout="column" fxLayoutAlign="center center" fxLayoutGap="20px">
<div fxFlex>
<h1>Lista de espacios</h1>
<app-form-buscar (onSearchEspacio)="buscar($event)"></app-form-buscar> (1)
</div>
<div fxLayoutAlign="center stretch">
<app-tabla-espacios [espacios]="espacios"></app-tabla-espacios>
</div>
</div>
1 | Llamar al método buscar() tras la publicación del evento onSearchEspacio pasándole los datos recibidos en el evento $event . |
La utilización de un evento publicado por parte de otro componente se hace siguiendo este patrón: Llamar a un método como respuesta al evento publicado y pasándole al método como parámetro los datos recibidos a través de un objeto
El código anterior escucha a un evento |
Si ahora volvemos a seleccionar Aulario II
y Aula 1
como criterios de búsqueda y pulsamos Buscar
vemos que ya sí se filtran los resultados.
10. Cuadros de diálogo
Un aspecto útil que debemos controlar es la presentación de información en cuadros de diálogo. Veamos en esta sección cómo tratar con ellos.
10.1. Cuadro de diálogo informativo
Comenzaremos creando un cuadro de diálogo informativo, que son los más sencillos. A modo de ejemplo, se trata de un cuadro de diálogo con información del proyecto. Se abrirá al pulsar un botón Créditos
que vamos a añadir a la barra lateral.
Para usar cuadros de diálogo es necesario el módulo Material |
Comenzaremos creando el componente para el cuadro de diálogo. Como es algo informativo y común a toda la aplicación no lo incluiremos en la carpeta main
. Lo colocaremos en shared/sidebar/components
.
$ ng g component shared/sidebar/components/dialogoCreditos
A continuación vamos a implementar la lógica del cuadro de diálogo (p.e. qué hacer al aceptar). Para ello, implementaremos un método que denominaremos aceptar()
a modo de prueba. Necesitaremos trabajar un objeto MatDialogRef
.
De acuerdo con la documentación de Angular Material, al abrir un cuadro de diálogo, éste devolverá una instancia de |
Archivo shared/sidebar/components/dialogo-creditos/dialogo-creditos.ts
:
import { Component, OnInit } from '@angular/core';
import { MatDialogRef } from '@angular/material/dialog';
@Component({
selector: 'app-dialogo-creditos',
templateUrl: './dialogo-creditos.component.html',
styleUrls: ['./dialogo-creditos.component.css'],
})
export class DialogoCreditosComponent implements OnInit {
constructor(public dialogRef: MatDialogRef<DialogoCreditosComponent>) {} (1)
ngOnInit(): void {}
aceptar() { (2)
console.log('Pulsaste Aceptar'); (3)
this.dialogRef.close(); (4)
}
}
1 | Inyección del handle para manejar el cuadro de diálogo |
2 | Método al que llamar cuando se pulse el botón Aceptar del cuadro de diálogo |
3 | Mostrar un eco en la consola para saber que el método se ejecutó. |
4 | Cerrar el cuadro de diálogo. |
A continuación crearemos el contenido del cuadro de diálogo (p.e. información del proyecto). Usaremos las directivas de estilo para los cuadros de diálogo de Material (p.e. mat-dialog-title, mat-dialog-content, mat-dialog-actions
y el estilo mat-typography
).
Archivo shared/sidebar/components/dialogo-creditos/dialogo-creditos.html
:
<h1 mat-dialog-title>Créditos</h1>
<div mat-dialog-content class="mat-typography">
<h3>Dirección del proyecto</h3>
<p>
Culpa incididunt ut occaecat amet dolore eiusmod ex ut sit laborum in
nostrud.
</p>
<h3>Equipo de desarrollo</h3>
<p>
Laboris excepteur voluptate deserunt labore ex consequat nisi fugiat non
incididunt adipisicing nisi ipsum labore.
</p>
<h3>Información del proyecto</h3>
<p>
<a href="https://www.ual.es" target="_blank">Web del proyecto</a>
</p>
</div>
<div mat-dialog-actions>
<button mat-button [mat-dialog-close] cdkFocusInitial>Cancelar</button> (1)
<button mat-button mat-flat-button color="primary" (click)="aceptar()"> (2)
Aceptar
</button>
</div>
1 | Botón Cerrar al que asociaremos directamente la directiva de cierre del formulario |
2 | Botón Aceptar que llamará al método que hemos creado un poco más arriba. |
A continuación, añadiremos a la barra lateral la lógica que abrirá el cuadro de diálogo
Archivo shared/sidebar/sidebar.component.ts
:
import { Component, OnInit } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { DialogoCreditosComponent } from './components/dialogo-creditos/dialogo-creditos.component';
@Component({
selector: 'app-sidebar',
templateUrl: './sidebar.component.html',
styleUrls: ['./sidebar.component.css'],
})
export class SidebarComponent implements OnInit {
constructor(public dialog: MatDialog) {} (1)
ngOnInit(): void {}
creditos() { (2)
const dialogRef = this.dialog.open(DialogoCreditosComponent, { (3)
width: '50%',
});
}
}
1 | Inyección del objeto MatDialog para poder abrirlo |
2 | Método que será llamado para abrir el cuadro de diálogo |
3 | Abrir el cuadro de diálogo y que ocupe el 50% del ancho de la pantalla |
Por último, ya sólo falta añadir a la barra lateral el botón que abre el cuadro de diálogo y asociarle el método creditos()
que acabamos de crear.
Archivo shared/sidebar/sidebar.component.html
:
<div fxLayout="column">
<button mat-button routerLink="/">Home</button>
<hr />
<mat-accordion>
...
</mat-accordion>
<button mat-button (click)="creditos()">Créditos</button> (1)
</div>
1 | Botón que abre el cuadro de diálogo |
La figura siguiente ilustra el resultado.
Hemos creado este cuadro informativo con un botón de cerrar y otro de aceptar al que le hemos añadido comportamiento. Un cuadro de información real sólo tendría un botón para cerrar. Hemos añadido otro botón al que asignar comportamiento para aprender a tratar con ello, aunque en este caso sea irrelevante. |
11. Eliminación de solicitudes
En esta sección vamos a ver cómo podemos realizar eliminaciones en nuestra aplicación. Para ello, dado que en la sección anterior hemos visto cómo presentar un cuadro de diálogo, nos apoyaremos en esa técnica para mostrar en un cuadro de diálogo informativo los datos que se van a eliminar antes de proceder a su eliminación.
La forma de proceder para la eliminación de solicitudes es incluir en el listado de solicitudes un botón de eliminar al lado de cada solicitud. El botón llamará a un método de eliminación al que pasaremos todos los datos de la solicitud que se vaya a eliminar. Con esos datos se podrá completar un cuadro de diálogo que permitirá la confirmación o la cancelación de la eliminación.
11.1. Creación del cuadro de diálogo de confirmación de eliminación.
Comencemos creando el componente del cuadro de diálogo de eliminación. Se trata de un componente que estará dentro de la carpeta de solicitudes.
Como convenio para nuestras aplicaciones de UAL-STIC, la eliminación tendrá su propio componente y se realizará mediante un cuadro de diálogo. Ese componente de eliminación se situará dentro de una carpeta |
$ ng g component main/solicitudes/components/dialogoEliminar
Usaremos los módulos |
11.2. Creación del método de eliminación de solicitudes
A continuación debemos implementar en el servicio Solicitudes
el método de eliminación de solicitudes añadiéndolo a services/solicitudes.service.ts
:
import { environment } from './../../environments/environment';
import { Observable } from 'rxjs';
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root',
})
export class SolicitudesService {
constructor(private http: HttpClient) {}
findAll(): Observable<any> {
return this.http.get(`${environment.urlEspacios}/solicitudes`);
}
save(data: any): Observable<any> {
return this.http.post(`${environment.urlEspacios}/solicitudes`, data);
}
delete(id: any): Observable<any> { (1)
return this.http.delete(`${environment.urlEspacios}/solicitudes/${id}`); (2)
}
}
1 | Método de eliminación de solicitudes |
2 | Llamada al método HTTP de eliminación componiendo la URL |
11.3. Programación de la lógica del cuadro de diálogo de eliminación
El cuadro de diálogo de eliminación usará un handle para manejar (en nuestro caso cerrar) el cuadro de diálogo y se le inyectarán los datos que queremos mostrar (en nuestro caso los datos de la solicitud a eliminar).
También usará el servicio de solicitudes para proceder a la eliminación, una snackbar para la presentación de mensajes y un router para redirigir la aplicación a otra pantalla tras la eliminación.
Archivo main/solicitudes/components/dialogo-eliminar/dialogo-eliminar.component.ts
:
import { Component, Inject, OnInit } from '@angular/core';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { Router } from '@angular/router';
import { SolicitudesService } from '../../../../services/solicitudes.service';
import { MatSnackBar } from '@angular/material/snack-bar';
@Component({
selector: 'app-dialogo-eliminar',
templateUrl: './dialogo-eliminar.component.html',
styleUrls: ['./dialogo-eliminar.component.css'],
})
export class DialogoEliminarComponent implements OnInit {
constructor(
public dialogRef: MatDialogRef<DialogoEliminarComponent>, (1)
@Inject(MAT_DIALOG_DATA) public data: any, (2)
private solicitudesService: SolicitudesService, (3)
private router: Router, (4)
private snackBar: MatSnackBar (5)
) {}
ngOnInit(): void {}
eliminar() { (6)
this.solicitudesService.delete(this.data.id).subscribe(() => { (7)
this.snackBar.open('Solicitud eliminada', '', { (8)
duration: 1500,
});
this.dialogRef.close(); (9)
this.router.navigate([this.router.url]).then(() => { (10)
window.location.reload();
});
});
}
}
1 | Inyección del handle para manejar el cuadro de diálogo |
2 | Inyección de datos para el cuadro de diálogo |
3 | Servicio de operaciones con solicitudes |
4 | Router para desviar la aplicación a otra página tras la eliminación |
5 | Snackbar para la presentación de mensajes |
6 | Método para eliminación de solicitudes |
7 | Llamada al método eliminación del servicio |
8 | Presentación de la snackbar informativa |
9 | Cierre del cuadro de diálogo |
10 | Recargar la ruta de la página |
11.4. Presentación de los datos en el cuadro de diálogo de eliminación
Tal y como hemos creado en el apartado anterior, hemos dejado preparada la parte TypeScript del cuadro de diálogo con una variable data
que contiene los datos que se van a eliminar. Los mostraremos en el cuadro de diálogo para que los usuarios pueden comprobar lo que van a eliminar antes de proceder.
Archivo main/solicitudes/components/dialogo-eliminar/dialogo-eliminar.component.html
:
<h1 mat-dialog-title>Confirmación de eliminación</h1>
<div mat-dialog-content class="mat-typography">
<h3>Desea eliminar esto?</h3>
<p>Reserva para : {{ data.nombre }} - {{ data.cargo }}</p> (1)
<p>Actividad: {{ data.nombreActividad }}</p>
<p>
Desde {{ data.start | date: "dd/MM/yyyy" }} hasta
{{ data.end | date: "dd/MM/yyyy" }}. Día: {{ data.dia }}. Hora:
{{ data.horaInicio }} - {{ data.horaFin }}
</p>
</div>
<div mat-dialog-actions>
<button mat-button [mat-dialog-close] cdkFocusInitial>Cancelar</button> (2)
<button mat-button mat-flat-button color="warn" (click)="eliminar()"> (3)
Eliminar
</button>
</div>
1 | Presentación de elementos nombre y cargo del objeto data |
2 | Acción de cierre del cuadro proporionada por [mat-dialog-close] |
3 | Llamada al método eliminar() implementado en la parte TypeScript del cuadro de diálogo |
11.5. Programación de la llamada a la eliminación desde el listado de solicitudes.
Ahora se tarta de programar la eliminación desde el listado. Se trata de implementar un método que abra el cuadro de diálogo y que le pase los datos de eliminación para poder mostrar a los usuarios lo que van a eliminar antes de proceder.
Archivo main/solicitudes/pages/consultar/consultar.component.ts
:
import { Component, OnInit } from '@angular/core';
import { SolicitudesService } from '../../../../services/solicitudes.service';
import { MatSnackBar } from '@angular/material/snack-bar';
import { MatDialog } from '@angular/material/dialog';
import { DialogoEliminarComponent } from '../../components/dialogo-eliminar/dialogo-eliminar.component';
@Component({
selector: 'app-consultar',
templateUrl: './consultar.component.html',
styleUrls: ['./consultar.component.css'],
})
export class ConsultarComponent implements OnInit {
solicitudes: any = [];
dataSource: any = [];
displayedColumns = ['nombre', 'cargo', 'unidad', 'telefono', 'acciones'];
constructor(
private solicitudesService: SolicitudesService, (1)
private snackBar: MatSnackBar, (2)
public dialog: MatDialog (3)
) {}
ngOnInit(): void {
this.solicitudesService.findAll().subscribe((res) => {
this.dataSource = res;
if (this.dataSource.length == 0) {
this.snackBar.open('No hay solicitudes', '', {
duration: 1500,
});
}
});
}
eliminar(element: any) { (4)
this.dialog.open(DialogoEliminarComponent, { (5)
data: element, (6)
});
}
}
1 | Servicio en el que está el método de eliminación de solicitudes |
2 | Snackbar para la presentación de mensajes de error |
3 | Inyección del cuadro de diálogo para poder abrirlo |
4 | Método eliminar() llamado desde la página del listado de solicitudes |
5 | Abrir el cuadro de diálogo pasándole datos en data |
6 | Configurar data con los datos que reciba el método |
11.6. Presentación del botón de eliminación en el listado de solicitudes
Por último, ya sólo nos falta incluir un botón de eliminar en el componente del listado de solicitudes, y que llame al método que presenta el cuadro de diálogo de eliminación pasándole los datos de la solicitud a eliminar.
Archivo main/solicitudes/pages/consultar/consultar.component.html
:
<div fxLayout="column" fxLayoutAlign="center center">
<h1>Listado de solicitudes</h1>
<div *ngIf="dataSource.length > 0">
<hr />
<mat-card>
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8">
<ng-container matColumnDef="nombre">
<th mat-header-cell *matHeaderCellDef>Nombre</th>
<td mat-cell *matCellDef="let element">{{ element.nombre }}</td>
</ng-container>
<ng-container matColumnDef="cargo">
<th mat-header-cell *matHeaderCellDef>Cargo</th>
<td mat-cell *matCellDef="let element">{{ element.cargo }}</td>
</ng-container>
<ng-container matColumnDef="unidad">
<th mat-header-cell *matHeaderCellDef>Unidad</th>
<td mat-cell *matCellDef="let element">{{ element.unidad }}</td>
</ng-container>
<ng-container matColumnDef="telefono">
<th mat-header-cell *matHeaderCellDef>Teléfono</th>
<td mat-cell *matCellDef="let element">{{ element.telefono }}</td>
</ng-container>
<!-- Columna de accciones -->
<ng-container matColumnDef="acciones"> (2)
<th mat-header-cell *matHeaderCellDef>Acciones</th>
<td mat-cell *matCellDef="let element"> (3)
<button
mat-stroked-button
color="primary"
(click)="eliminar(element)" (4)
>
<mat-icon>delete</mat-icon> (5)
Eliminar
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
</table>
</mat-card>
</div>
</div>
1 | Mostrar la tabla si hay datos |
2 | Bloque para la columna de acciones |
3 | Configuración del valor de la celda a todo el registro mostrado (element ) |
4 | Llamada al método eliminar() de la parte TypeScript pasándole todo el registro |
5 | Incorporación al botón del icono de la papelera |
Veamos el resultado en funcionamiento. Partimos de una pantalla con un conjunto de solicitudes. Al lado de cada solicitud aparecerá un botón para poder eliminarla.
Si pulsamos Eliminar
sobre una de ellas aparecerá el cuadro de diálogo de confirmación mostrando los datos para comprobar que es la solicitud a eliminar.
Si confirmamos la eliminación, se eliminará la solicitud, se informará en una snackbar y se devolverá la aplicación para que vuelva a mostrar la lista de solicitudes. La solicitud eliminada ya no aparecerá.
12. Actualización de solicitudes
Para finalizar con la parte de operaciones CRUD vamos a añadir a la aplicación la posibilidad de actualizar solicitudes. Tendremos que:
-
Implementar los métodos de búsqueda y modificación de una solicitud.
-
Crear de una nueva ruta en la aplicación para la operación de modificación.
-
Adaptar el componente de crear para que también valga para modificar.
-
Crear el método de editar solicitud en el componente de listado de solicitudes.
-
Añadir el botón de modificar solicitud y conectarlo al método de editar solicitud anterior.
12.1. Creación del método de actualización de una solicitud
La actualización de una solicitud realmente implica dos operaciones diferentes. Una de búsqueda de la solicitud para la recuperación de sus datos y presentarlos en el formulario de modificación. Otra que se encargue de la modificación en sí misma. De esto se van a encargar los métodos findOne
y update
, respectivamente, que se muestran a continuación.
Archivo services/solicitudes.service.ts
:
import { environment } from './../../environments/environment';
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
@Injectable({
providedIn: 'root',
})
export class SolicitudesService {
constructor(private http: HttpClient) {}
findAll(): Observable<any> {
return this.http.get(`${environment.urlEspacios}/solicitudes`);
}
findOne(id: any): Observable<any> { (1)
return this.http.get(`${environment.urlEspacios}/solicitudes/${id}`); (2)
}
save(data: any): Observable<any> {
return this.http.post(`${environment.urlEspacios}/solicitudes`, data);
}
delete(id: any): Observable<any> {
return this.http.delete(`${environment.urlEspacios}/solicitudes/${id}`);
}
update(data: any): Observable<any> { (3)
return this.http.put(
`${environment.urlEspacios}/solicitudes/${data.id}`, (4)
data
);
}
}
1 | Método para la búsqueda y recuperación de los datos de la solicitud a modificar |
2 | Llamada al endpoint de recuperación de la solicitud a modificar |
3 | Método de actualización |
4 | Llamada al endpoint de actualización de la solicitud pasándole los datos de la solicitud modificada |
12.2. Creación de la ruta de modificación de solicitudes
Necesitamos una nueva ruta para editar una solicitud. A esa ruta asociaremos el componente que se mostrará en la aplicación. Realmente no es necesario un nuevo componente de Modificar solicitud. Se puede encargar el mismo componente de Crear solicitud. Eso sí, habrá que adaptarlo. Básicamente es lo mismo pero incluyendo el id
de la solicitud a modificar. Además, tendrá que incorporarse un método PATCH
para la modificación. Lo veremos más adelante. Por ahora, comencemos creando la nueva ruta para modificar una solicitud
Cuando creamos el módulo de rutas de solicitudes no añadimos una ruta específica para modificar. Esto se debió porque hasta ese momento cada ruta estaba asociada a un componente diferente. Para no provocar confusión en aquel momento llevando dos rutas a un mismo componente, se optó por no incluir la ruta de modificación de solicitudes en su módulo de rutas y posponer su creación hasta este momento. |
Archivo main/solicitudes/solictudes.routing.ts
:
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { CrearComponent } from './pages/crear/crear.component';
import { ConsultarComponent } from './pages/consultar/consultar.component';
const routes: Routes = [
{
path: '',
children: [
{ path: 'crear', component: CrearComponent },
{ path: 'consultar', component: ConsultarComponent },
{ path: 'editar/:id', component: CrearComponent }, (1)
{ path: '', redirectTo: 'consultar' },
],
},
{
path: '**',
redirectTo: 'crear',
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class SolicitudesRoutingModule {}
1 | Nueva ruta para modificación de solicitudes mostrando el componente de crear solicitudes |
Si ahora escribimos la ruta http://localhost/solicitudes/editar/1
comprobamos que la ruta es aceptada. Además, se muestra el componente de Crear solicitud, tal y como hemos indicado en el archivo de rutas.
Sin embargo, lo esperado en la aplicación es que hubiese mostrado una pantalla de modificación con los datos de la solicitud a modificar. Obviamente esto aún no se puede mostrar porque aún está por implementar. Veamos cómo hacerlo en los apartados siguientes.
12.3. Un componente para crear y editar
Hasta ahora tenemos un componente para crear solicitudes. Lo inmediato sería crear uno nuevo para editar solicitudes. Sin embargo, si nos paramos a pensar, el componente de Editar es prácticamente igual al de Crear salvo que al editar se cuenta con el id
de la solicitud que se está editando, y al crear, el id
es creado a posteriori. Por tanto, podríamos plantearnos en tener un único componente que usaríamos para las dos operaciones. En ese caso, tendremos que usar una variable que indicará si estamos en modo de edición o en modo de creación. En función de su valor, se mostrará un texto u otro en la cabecera y en los botones (texto de creación o de modificación), y se llamará al método correspondiente (de creación o de eliminación). Veamos cómo hacerlo.
Comencemos con la adaptación de la parte TypeScript del componente de Crear solicitud para que también valga para editar.
Archivo main/solicitudes/pages/crear/crear.component.ts
:
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { lastValueFrom } from 'rxjs';
import { PersonaService } from '../../../../services/persona.service';
import { MatSnackBar } from '@angular/material/snack-bar';
import { Solicitud } from '../../../../interfaces/solicitud';
import { SolicitudesService } from '../../../../services/solicitudes.service';
import { ActivatedRoute, Router } from '@angular/router';
@Component({
selector: 'app-crear',
templateUrl: './crear.component.html',
styleUrls: ['./crear.component.css'],
})
export class CrearComponent implements OnInit {
modoEditar: boolean = false; (1)
id: any; (2)
cargos: string[] = [];
persona: any;
formHorario: FormGroup = this.fb.group({
nombre: [{ value: '', disabled: true }],
cargo: [, [Validators.required]],
unidad: [{ value: '', disabled: true }],
telefono: [{ value: '', disabled: true }],
email: [, [Validators.required, Validators.email]],
tipo: [,],
nombreActividad: [, [Validators.required, Validators.minLength(5)]],
start: [,],
end: [,],
dia: [,],
horaInicio: [,],
horaFin: [,],
});
diasSemana: string[] = [
'lunes',
'martes',
'miercoles',
'jueves',
'viernes',
'sabado',
'domingo',
];
horas = Array.from(Array(24).keys());
constructor(
private fb: FormBuilder,
private personaService: PersonaService,
private solicitudesService: SolicitudesService,
private snackBar: MatSnackBar,
private router: Router,
private route: ActivatedRoute (3)
) {}
ngOnInit(): void {
this.modoEditar = false; (4)
this.id = this.route.snapshot.params['id']; (5)
if (this.id) { (6)
this.solicitudesService.findOne(this.id).subscribe((res) => { (7)
this.modoEditar = true; (8)
this.formHorario.patchValue({ ...res }); (9)
});
}
}
isNotValidField(field: string) {
return (
this.formHorario.controls[field].errors &&
this.formHorario.controls[field].touched
);
}
isValidForm() {
return this.formHorario.valid;
}
buscarPersona(email: any) {
return lastValueFrom(this.personaService.findOne(email));
}
async actualizarCamposPersona() {
const email = this.formHorario.controls['email'].value;
this.persona = (await this.buscarPersona(email))[0];
if (this.persona) {
this.cargos = [...this.persona.cargo];
this.formHorario.patchValue(this.persona);
this.persona.cargo = this.formHorario.controls['cargo'].value;
return;
}
this.clearPersonalData();
}
clearPersonalData() {
this.formHorario.reset();
this.snackBar.open('Persona no disponible', '', {
duration: 1500,
});
}
save() {
let solicitud = this.formHorario.getRawValue();
if (this.modoEditar) { (10)
(solicitud.id = this.id), (11)
this.solicitudesService.update(solicitud).subscribe(() => { (12)
this.snackBar.open('Solicitud actualizada', '', {
duration: 1500,
});
this.router.navigate(['/solicitudes/consultar']);
});
} else { (13)
this.solicitudesService.save(solicitud).subscribe((res) => {
this.snackBar.open('Solicitud guardada', '', {
duration: 1500,
});
this.router.navigate(['/solicitudes/consultar']);
});
}
}
}
1 | Variable para indicar que se está en modo Editar |
2 | Variable para almacenar el id de la solicitud |
3 | Permite acceder a componentes de la URL (necesitamos acceder al id de la solicitud a editar) |
4 | Configurar el modo Crear como el modo predeterminado (editar es falso) |
5 | Asignar a la variable id el componente id de la ruta de la URL. Las partes de la ruta están definidas en el módulo de rutas solicitudes-routing.module.ts |
6 | Si hay id en la URL (es decir, se está en modo Editar) |
7 | Buscar la solicitud mediante su id |
8 | Activar el modo Editar |
9 | Pegarle a cada elemento del formulario los elementos de la solicitud leída |
10 | Bloque a ejecutar cuando se está en modo Editar |
11 | ??? |
12 | Llamada al método de actualizar pasándole la solicitud como parámetro |
13 | Bloque a ejecutar cuando se está en modo Crear |
Para asignar cada elemento de una solicitud a su elemento correspondiente del formulario se puede hacer de uno en uno, pero resulta muy pesado y es propenso a errores. En cambio, si los nombres de los elementos del formulario coinciden con los nombre de los elementos de la solicitud leída de la base de datos, se puede hacer con una sola línea mediante |
Ahora tocaría modificar la parte de la presentación del formulario de Crear solicitud para que sirva también para actualizar. Básicamente se trata de cambiar el texto del título de la página y el del botón. El método al que se llama para guardar sigue siendo el mismo, ya que el propio método actualizará o insertará en función del valor de modoEditar
, que recordemos que se determinaba a partir de lo que indicase la ruta.
Archivo main/solicitudes/pages/crear/crear.component.html
:
<div fxFlexAlign="center" fxLayoutAlign="center center">
<form [formGroup]="formHorario" autocomplete="off">
<h1>{{ modoEditar ? "Editar" : "Crear" }} solicitud</h1> (1)
<hr />
....
<div fxLayout="row" fxLayoutAlign="end">
<button
mat-stroked-button
color="primary"
(click)="save()"
[disabled]="!isValidForm()"
>
{{ modoEditar ? "Actualizar" : "Crear" }} (2)
</button>
</div>
</mat-card>
</div>
</form>
</div>
1 | Añadir Editar o Crear a la cabecera en función del valor de modoEditar |
2 | Configurar el texto del botón con Actualizar o Crear en función del valor de modoEditar |
12.4. Actualizar el componente del listado de solicitudes
Para terminar, tendremos que añadir el botón de modificar al listado de solicitudes y programar el método que se ejecutará al pulsar el botón. Comencemos programando la llamada al método de actualización. Básicamente, lo que hará será redirigir a la ruta donde está el componente de editar/crear.
Archivo main/pages/solicitudes/consultar/consultar.component.ts
:
import { Component, OnInit } from '@angular/core';
import { SolicitudesService } from '../../../../services/solicitudes.service';
import { MatSnackBar } from '@angular/material/snack-bar';
import { MatDialog } from '@angular/material/dialog';
import { DialogoEliminarComponent } from '../../components/dialogo-eliminar/dialogo-eliminar.component';
import { Router } from '@angular/router'; (1)
@Component({
selector: 'app-consultar',
templateUrl: './consultar.component.html',
styleUrls: ['./consultar.component.css'],
})
export class ConsultarComponent implements OnInit {
solicitudes: any = [];
dataSource: any = [];
displayedColumns = ['nombre', 'cargo', 'unidad', 'telefono', 'acciones'];
constructor(
private solicitudesService: SolicitudesService,
private snackBar: MatSnackBar,
public dialog: MatDialog,
private router: Router (2)
) {}
ngOnInit(): void {
this.solicitudesService.findAll().subscribe((res) => {
this.dataSource = res;
if (this.dataSource.length == 0) {
this.snackBar.open('No hay solicitudes', '', {
duration: 1500,
});
}
});
}
editar(element: any) { (3)
this.router.navigate([`/solicitudes/editar/${element.id}`]); (4)
}
eliminar(element: any) {
this.dialog.open(DialogoEliminarComponent, {
data: element,
});
}
}
1 | Importación de `Router |
2 | Inyección del Router |
3 | Método llamado al pulsar el botón de Editar |
4 | Redirigir a la ruta de editar solicitudes |
Finalizaremos añadiendo un botón de Actualización de solicitudes en la pantalla de Consultar solicitudes. El botón llamará al método de actualización que acabamos de crear.
Archivo main/pages/solicitudes/consultar/consultar.component.html
:
<div fxLayout="column" fxLayoutAlign="center center">
<h1>Listado de solicitudes</h1>
<div *ngIf="dataSource.length > 0">
<hr />
<mat-card>
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8">
<ng-container matColumnDef="nombre">
<th mat-header-cell *matHeaderCellDef>Nombre</th>
<td mat-cell *matCellDef="let element">{{ element.nombre }}</td>
</ng-container>
<ng-container matColumnDef="cargo">
<th mat-header-cell *matHeaderCellDef>Cargo</th>
<td mat-cell *matCellDef="let element">{{ element.cargo }}</td>
</ng-container>
<ng-container matColumnDef="unidad">
<th mat-header-cell *matHeaderCellDef>Unidad</th>
<td mat-cell *matCellDef="let element">{{ element.unidad }}</td>
</ng-container>
<ng-container matColumnDef="telefono">
<th mat-header-cell *matHeaderCellDef>Teléfono</th>
<td mat-cell *matCellDef="let element">{{ element.telefono }}</td>
</ng-container>
<!-- Columna de accciones -->
<ng-container matColumnDef="acciones">
<th mat-header-cell *matHeaderCellDef>Acciones</th>
<td mat-cell *matCellDef="let element">
<button (1)
mat-stroked-button
color="primary"
(click)="editar(element)" (2)
>
<mat-icon>edit</mat-icon>
Editar
</button>
<button
mat-stroked-button
color="primary"
(click)="eliminar(element)"
>
<mat-icon>delete</mat-icon>
Eliminar
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
</table>
</mat-card>
</div>
</div>
1 | Botón de modificación |
2 | Llamada al método de modificación pasándole la solicitud a modificar |
Veamos el resultado en funcionamiento. Partimos de una pantalla con un conjunto de solicitudes. Al lado de cada solicitud aparecerá un botón para poder modificarla.
Si pulsamos el botón Editar
de la solicitud a modificar nos redirigirá a la pantalla de edición de solicitudes. En la cabecera aparecerá Editar solicitud
ya que se ha entrado al componente en modo Editar. Recordemos que se trata del componente de Crear solicitud que hemos modificado para que sirva tanto para crear como para modificar solicitudes.
Aprovecharemos y haremos una modificación en el cargo con el realiza la solicitud. Vemos que también aparece modificado el texto del botón, que ahora es Editar
. Si pulsamos el botón nos llevará al listado de solicitudes donde aparecerá la solicitud modificada.
Si ahora seleccionáramos la opción de crear solicitud veríamos que se muestra correctamente la pantalla de Crear. Ahora la cabecera y el botón son los correspondientes a la creación de solicitudes.
Anexo I. Creación de un servidor JSON para datos de prueba
Con el fin de poder simular el funcionamiento de servicios de backend sin necesidad de montar un backend y su complejidad asociada, para desarrollar la aplicación en Angular podemos usar algo con menor funcionalidad pero que nos permita realizar nuestras operaciones CRUD básicas.
JSON-server nos ofrece la posibilidad de tener de forma muy sencilla un prototipo de API REST totalmente funcional sin necesidad de programar nada.
Se instala de forma sencilla en nuestro equipo de trabajo con
$ npm install -g json-server
A continuación, hay que crear un archivo JSON con los datos que va a manejar la API. Después iniciamos JSON Server con
$ json-server --watch db.json
Incluiremos un archivo db.json
en nuestro proyecto para que contemos con un conjunto inicial de datos.
{
"personas": [
{
"email": "mtorres@ual.es",
"nombre": "Manuel Torres Gil",
"telefono": "84030",
"unidad": "Departamento de Informática",
"cargo": [
"Profesor Titular de Universidad",
"Director de Secretariado de Innovación Tecnológica"
],
"docente": true
},
{
"email": "ggf906@ual.es",
"nombre": "Francisco José García García",
"telefono": "N/D",
"unidad": "STIC",
"cargo": [
"Gestor Informática"
],
"docente": false
}
],
"espacios": [
{
"edificio": "Aulario I",
"aula": "Aula 2",
"fecha": "07/02/2022",
"reservas": [
{
"hora": "09:00 - 12:00",
"descripcion": "GRUPO A",
"asignatura": "( 12103230 ) - Lengua Clásica: Latín",
"profesor": ""
}
]
},
{
"edificio": "Aulario I",
"aula": "Aula 1",
"fecha": "07/02/2022",
"reservas": [
{
"hora": "09:00 - 12:00",
"descripcion": "GRUPO UNICO",
"asignatura": "( 67104216 ) - Gestión Integral de la Imagen",
"profesor": ""
}
]
},
{
"edificio": "Aulario II",
"aula": "Aula 1",
"fecha": "07/02/2022",
"reservas": [
{
"hora": "09:00 - 12:00",
"descripcion": "GRUPO A",
"asignatura": "( 31103209 ) - Lexicología y Semántica Inglesas",
"profesor": ""
},
{
"hora": "12:00 - 15:00",
"descripcion": "GRUPO A",
"asignatura": "( 12104226 ) - Historia de la Lengua Española II",
"profesor": ""
},
{
"hora": "16:00 - 19:00",
"descripcion": "GRUPO UNICO",
"asignatura": "( 40153329 ) - Teoría de Códigos y Criptografía",
"profesor": ""
}
]
}
],
"solicitudes": []
}
El puerto predeterminado en que se ofrece la API REST es el 3000. Así, tendríamos una API REST funcional en:
http://localhost:3000/personas
http://localhost:3000/espacios
http://localhost:3000/solicitudes
Para iniciar JSON Server en otro puerto, pasamos al final el parámetro --port
seguido de un número de puerto.
$ json-server --watch db.json --port 3002
En este caso tendríamos la API REST funcional en
http://localhost:3002/personas
http://localhost:3002/espacios
http://localhost:3002/solicitudes