Crear una ventana emergente en Angular 7

A menudo necesitamos insertar datos en cualquier sistema. Normalmente, estos sistemas separan el Back con una API y el front con una aplicación construida en un framework como Angular, que separa la interfaz de usuario de la lógica de negocio. Vamos a ver cómo crear una ventana emergente o pop-up con Angular para interactuar con nuestros servicios.

En nuestro ejemplo tendremos una API construida con SpingBoot que nos permite visualizar una lista de cursos (con los datos correspondientes de su profesor, nivel, horas, etc) y, por supuesto, añadir nuevos cursos. Para escribir los datos que queremos insertar de un nuevo curso, lo más fácil sería optar por hacer otra vista y, con botones, enlazar las diferentes vistas. No obstante, cuando vamos a insertar pocos datos, ¿por qué no crear una ventana emergente o pop-up? Así el usuario no perderá de vista la venatana principal a la que hacen referencia los datos que está insertando.

Descargate el código fuente de este ejemplo en GitHub

Contexto

Nuestra API se encuentra en un servidor Tomcat local dockerizado. Para acceder a la API debemos hacer peticiones POST o GET a http://localhost:8080/courses. En nuestro caso, esta API nos devuelve un objeto Pageable, pero eso nos importa poco, ya que no estamos visualizando datos, sino que vamos a insertarlos.

ApiService

Como cualquier aplicación front que consume servicios de una API, tenemos nuestro servicio de conexión que, en nuestro caso, nos interesa lo siguiente:

// imports;

@Injectable({
  providedIn: 'root'
})
export class ApiService {

  private BASE_URI = 'http://localhost:8080';

  constructor(private http: HttpClient) {  }

  addCourse(course: Course) {
    try {
      return this.http.post<Course>(this.BASE_URI + '/courses', course);
    } catch (error) {
      this.logService.print(error, LogService.ERROR_MSG);
    }
  }
}

Por supuesto, previamente hemos debido importar las dependencias necesarias en el app.module.ts, pero vamos a partir de la base en la que ya conocemos cómo conectar una aplicación Angular con un backend consumiendo servicios de una API Rest. Vamos a crear una ventana emergente (Pop-up) desde la que insertaremos datos en la API usando este servicio.

CoursesComponent

Este es el componente raíz desde el que vamos a abrir la ventana emergente (pop-up) que vamos a crear. En ella debemos implementar un botón y un método que, al ejecutarse, levante un cuadro de diálogo con el componente que vamos a crear como contenido del pop-up.

Crear la ventana emergente o pop-up

Ahora ya llegamos al meollo de la cuestión. Generamos un componente que contendrá los elementos a presentar en nuestro diálogo. En mi caso, tengo una carpeta dentro del proyecto llamada components para los componentes, así que debo crear el componente dentro de la misma carpeta. Si no tienes, puedes crearlo en la raíz del proyecto sin más.

  • ng generate component components/PopupCourse</code

Y, como podemos ver, nos genera el componente deseado:

Pop-up component Angular 7 project

Muestra del componente creado en proyecto

Contenido

En el archivo html, como cualquier componente, insertamos aquello que deseemos visualizar.  Utilizamos bootstrap en todo el proyecto, así que debes importarlo junto a Material para poder obtener los mismos acabados.

<div style="width: 100%; height: 50px; margin-top: 1%; text-align: center" class="text-dark bg-light rounded">
  <h1 class="font-weight-bold">NUEVO CURSO</h1>
</div>

<mat-dialog-content>
  <div class="container" style="margin-top: 5%">
    Título del curso:
      <input type="text" class="form-control" placeholder="Titulo" [(ngModel)]="courseToInsert.title">
  </div>
  <div class="container" style="margin-top: 5%">
    Nivel:
      <select class="custom-select" [(ngModel)]="courseToInsert.level">
        <option value="Básico">Básico</option>
        <option selected value="Intermedio">Intermedio</option>
        <option value="Avanzado">Avanzado</option>
      </select>
  </div>
  <div class="container" style="margin-top: 5%">
    Horas de duración:
    <select class="custom-select" [(ngModel)]="courseToInsert.hours">
      <option *ngFor='let number of hoursArray' [value]=number>
        {{number}}
      </option>
    </select>
  </div>
  <div class="container" style="margin-top: 5%">
    Profesor:
    <select class="custom-select" [(ngModel)]="courseToInsert.teacher.id">
      <option *ngFor="let teacher of teachers" [value]="teacher.id">{{teacher.name}}</option>
    </select>
  </div>
  <div class="container" style="margin-top: 5%">
    Estado:
    <select class="custom-select" [(ngModel)]="courseToInsert.active">
      <option selected [value]=true>Activo</option>
      <option [value]="false">Inactivo</option>
    </select>
  </div>
</mat-dialog-content>

<mat-dialog-actions>
  <button type="button" class="btn btn-light" (click)="closeWithoutSave()">Close</button>
  <button [mat-dialog-close]="courseToInsert" style="margin-left: 5%" type="button" class="btn btn-dark">Save</button>
</mat-dialog-actions>

El esquema básico se compone de tres partes:

<!-- TÍTULO LIBRE, COMO QUIERAS -->

<mat-dialog-content>
    <!-- CONTENIDO DEL DIÁLOGO -->
</mat-dialog-content>

<mat-dialog-actions>
    <!-- ACCIONES (GENERALMENTE BOTONES). BAJO EL CONTENIDO ALINEADAS -->
</mat-dialog-actions>

El código que has visto muestra lo siguiente:

Dialog material angular 7 content example

Muestra del contenido mostrado por el diálogo

En nuestro caso, el contenido muestra un input de texto para el título del curso y varios selectores para establecer el resto de parámetros. Para guardarlos, como puedes ver en el código anterior, utilizamos la directiva [(ngModel)]="courseToInsert.valor". De esta forma podemos guardar cada valor dentro del objeto courseToInsert que se encuentra en la lógica de nuestro componente, en el archivo popup-course.component.ts.

Otro aspecto importante en el que fijarse es en el botón para guardar los datos. En este caso usamos la directiva [mat-dialog-close]="courseToInsert" para especificar que, al cerrar el diálogo, el objeto devuelto es courseToInsert. Por último, el botón Close llamará al método closeWithoutSave() que cerrará el diálogo sin retornar ningún objeto y, en nuestro caso, imprmirá un log visible desde la consola del navegador.

Lógica

El archivo con el código typescript necesario para crear nuestra ventana emergente en Angular debe contener los datos a insertar (en nuestro caso, courseToInsert), el método para cerrar sin guardar y, lo más importante, un objeto MatDialogRef inyectado. El código completo es el siguiente:

<div style="width: 100%; height: 50px; margin-top: 1%; text-align: center" class="text-dark bg-light rounded">
  <h1 class="font-weight-bold">NUEVO CURSO</h1>
</div>

<mat-dialog-content>
  <div class="container" style="margin-top: 5%">
    Título del curso:
      <input type="text" class="form-control" placeholder="Titulo" [(ngModel)]="courseToInsert.title">
  </div>
  <div class="container" style="margin-top: 5%">
    Nivel:
      <select class="custom-select" [(ngModel)]="courseToInsert.level">
        <option value="Básico">Básico</option>
        <option selected value="Intermedio">Intermedio</option>
        <option value="Avanzado">Avanzado</option>
      </select>
  </div>
  <div class="container" style="margin-top: 5%">
    Horas de duración:
    <select class="custom-select" [(ngModel)]="courseToInsert.hours">
      <option *ngFor='let number of hoursArray' [value]=number>
        {{number}}
      </option>
    </select>
  </div>
  <div class="container" style="margin-top: 5%">
    Profesor:
    <select class="custom-select" [(ngModel)]="courseToInsert.teacher.id">
      <option *ngFor="let teacher of teachers" [value]="teacher.id">{{teacher.name}}</option>
    </select>
  </div>
  <div class="container" style="margin-top: 5%">
    Estado:
    <select class="custom-select" [(ngModel)]="courseToInsert.active">
      <option selected [value]=true>Activo</option>
      <option [value]="false">Inactivo</option>
    </select>
  </div>
</mat-dialog-content>

<mat-dialog-actions>
  <button type="button" class="btn btn-light" (click)="closeWithoutSave()">Close</button>
  <button [mat-dialog-close]="courseToInsert" style="margin-left: 5%" type="button" class="btn btn-dark">Save</button>
</mat-dialog-actions>

En el constructor vemos que también recibe inyectado el servicio de la API. No es lo más adecuado. Os explico por qué: en este caso, la ventana emergente que vamos a crear con Angular necesita una lista de profesores que se encuentra en la base de datos y, por tanto, es accesible desde la API. De esta forma, el componente puede llamar a la API, obtener los profesores y mostrar en un selector el listado de profesores para que, a la hora de crear un curso nuevo, se pueda añadir un profesor existente desde dicho selector.

Pero claro, aquí seguramente estamos pasando por alto principios importantes como el de responsabilidad única. Lo ideal sería pasarle por constructor al componente del diálogo la lista de profesores y que el componente raíz se ocupara de crear la lista desde la API e inyectársela. No obstante, centrémonos en cómo abrir y gestionar la vida de una ventana emergente en Angular independientemente de cómo vayamos a tratar los datos. Eso sí, puede que el código os lo encontreis refactorizado en el repositorio, no te preocupes si no coincide con lo que te acabo de mostrar.

Estilos

Un detalle muy importante para que todo esto funcione adecuadamente: importar los estilos de Angular material para que, al crear nuestra ventana emergente en Angular, funcione de verdad como un pop-up. De no realizar la siguiente importación en el styles.css raíz de tu proyecto, no se verá como un pop-up, sino que aparecerá bajo todo el contenido de tu aplicación web:

@import "~@angular/material/prebuilt-themes/indigo-pink.css";

Guardar los datos en la API

Antes os expliqué cuál era la función del componente CoursesComponent como raíz o padre que levanta el pop-up. Vamos a ver ahora cómo se implementa. Lo vamos a hacer desde un botón, cuyo código os muestro a continuación:

<button type="button" class="btn btn-dark" (click)="openDialog()">NUEVO CURSO</button>

Los estilos del botón son lo de menos (Bootstrap se ocupa de ello). Lo que nos intersa es la directiva (click)="openDialog()" en la que ejecutamos ese método al hacer click. Este método se encarga de instanciar y levantar el componente que hemos creado antes como un cuadro de diálogo:

openDialog() {
    const dialogConfig = new MatDialogConfig();

    dialogConfig.disableClose = false;
    dialogConfig.autoFocus = true;

    const dialogRef = this.dialog.open(PopupCourseComponent, dialogConfig);

    dialogRef.afterClosed().subscribe(
      data => {
        if (!isUndefined(data)) {
          this.logService.print('CoursesComponent: data received from dialog', LogService.DEFAULT_MSG);
          if (this.verifyDataIntegrity(data)) {
            this.logService.printLogWithObject('Data integrity verification success. Course to insert:', data);
            this.saveNewCourse(data);
          } else {
            this.logService.print('Some data undefined. Cancel save', LogService.WARN_MSG);
          }
        } else {
          this.logService.print('Undefined data. No will insert', LogService.DEFAULT_MSG);
        }
      }, error => this.logService.print(error, LogService.ERROR_MSG));
  }

private verifyDataIntegrity(course: Course) {
    this.logService.print('Verifying data integrity...', LogService.DEFAULT_MSG);
    if (isUndefined(course.title)) {
      this.logService.print('course.title = undefined', LogService.WARN_MSG);
      return false;
    }
    if (isUndefined(course.teacher.id)) {
      this.logService.print('teacher.id = undefined', LogService.WARN_MSG);
      return false;
    }
    if (isUndefined(course.level)) {
      this.logService.print('course.level = undefined', LogService.WARN_MSG);
      return false;
    }
    if (isUndefined(course.hours)) {
      this.logService.print('course.hours = undefined', LogService.WARN_MSG);
      return false;
    }
    if (isUndefined(course.active)) {
      this.logService.print('course.active = undefined', LogService.WARN_MSG);
      return false;
    }
    return true;
  }

A ver, estos métodos tienen mucho código boilerplate que sólo sirve para logear los datos (ya que se trata de una aplicación de ejemplo para aprendizaje) y verificar la integridad de los datos. De todo ese código, os dejo la versión resumida que muestra la lógica pura y dura de nuestra acción de guardar:

openDialog() {
    const dialogConfig = new MatDialogConfig();

    dialogConfig.disableClose = false;
    dialogConfig.autoFocus = true;

    const dialogRef = this.dialog.open(PopupCourseComponent, dialogConfig);

    dialogRef.afterClosed().subscribe(
      data => {
                this.saveNewCourse(data);
      }, error => this.logService.print(error, LogService.ERROR_MSG));
  }

private saveNewCourse(courseToInsert: Course) {
    this.apiService.addCourse(courseToInsert).subscribe();
  }

ngOnDestroy(): void {
    this.sub.unsubscribe();
  }

Clase MatDialog

Al principio del método podemos observar que se crea una instancia de MatDialogConfig y que se sobreescriben algunos parámetros a continuación. Esta primera parte no es obligatoria, pero sí es buena porque muestra cómo poder editar los parámetros que nos permitirán configurar programáticamente cómo queremos crear nuestra ventana emergente desde Angular. Si no hacemos nada, se inicializarán por defecto. Esos dos parámetros configurados no son los únicos que puedes toquetear. te recomiendo que le eches un vistazo a la documentación oficial de Angular Material si te interesa.

Abrimos el diálogo con this.dialog.open(PopupCourseComponent, dialogConfig). Se entiende fácilmente que estamos creando un diálogo emergente cuyo contenido será el componente PopupCourseComponent y la configuración previamente establecida en el objeto dialogConfig. Guardamos lo que retorna el método open() en una variable (o constante) para poder suscribirnos posteriormente a él gracias a los observadores de Angular. Por último nos suscribimos y, después de cerrar, guardamos lo que nos retorna (recordemos el objeto courseToInsert) y llamamos al método propio que usa el ApiService para insertar el nuevo curso.

Por último, con el método ngOnDestroy(), nos aseguramos de que, tras cerrar la ventana emergente, se ha cerrado también la suscripción.

Conclusión

Con todo lo que hemos hecho, ya tenemos un componente que puede ser insertado al levantar un cuadro de diálog y podemos obtener del mismo los datos introducidos por el usuario para, posteriormente, tratarlos como nos plazca. En nuestro caso, los hemos insertado con un HTTP POST en nuestra propia API, pero tú puedes dejar volar tu creatividad para saber qué hacer con ellos.

 

 

 

2 comentarios

  1. Eloy

    Hola amigo
    que pasa si quiero seguir ingresando datos sin salir del modal, para que los datos del modal se vea reflejado en el componente padre o listado desde donde se hace open al dialog
    por que imagínate si ingresas 50 registros, se hace tedioso estar haciendo click en agregar constantemente ya que al guardar se sale del modal y sucesivamente

    Saludos

  2. Gerardo CP

    Hola amigo buenas tardes, una propuesta seria que intentaras reseteando los campos una vez que ingresaste los datos, esto lo puedes hacer desde el método o función que le asignaste a tu botón.
    Saludooosss!

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

*