¿Qué aprenderá?

Este tutorial lo guía, usando VSCode, en la construcción de una aplicación Angular compuesta del módulo principal y de un módulo llamado BookModule el cual declara un componente BookList para desplegar un catálogo de libros.

¿Qué construirá?

Una aplicación desarrollada en el framework Angular que despliega la lista descrita en la Figura 1.

Figura 1. Aplicación que se desarrolla en este tutorial.

¿Qué necesita?

En particular usted debe saber cómo crear:

Todo esto utilizando Angular CLI desde terminal.

Crear proyecto nuevo

Cree un proyecto nuevo en Angular siguiendo los pasos aprendidos en el tutorial Tutorial Creación Aplicación Vacía Angular

Adicionalmente, en este proyecto vacío instale Bootstrap, cree los archivos de environments y cree los interceptor como aprendió en el tutorial Tutorial Aplicación Básica. No debe ejecutar todos los pasos del tutorial.

Crear un nuevo módulo

Para crear el nuevo módulo utilizamos el comando:

ng generate module book --type-separator=.

y confirma que se creó la carpeta book dentro de app como se muestra en la Figura 2.

Figura 2. Módulo book creado.

Incluir el nuevo módulo en el principal

Para que la aplicación pueda utilizar el nuevo módulo, este se debe importar en el módulo principal AppModule.

Para importar en el módulo principal el módulo de BookModule debe:

Al final el archivo app-module.ts debe quedar así:

import { NgModule, provideBrowserGlobalErrorListeners } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { AppRoutingModule } from './app-routing-module';
import { App } from './app';
import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http';
import { ToastrModule } from 'ngx-toastr';
import { HttpErrorInterceptorService } from './interceptors/http-error-interceptor.service';
import { BookModule } from './book/book.module';

@NgModule({
  declarations: [App],
  imports: [
    BrowserModule,
    AppRoutingModule,
    HttpClientModule,
    ToastrModule.forRoot({
      timeOut: 10000,
      positionClass: 'toast-bottom-right',
      preventDuplicates: true,
    }),
    BookModule,
  ],
  providers: [
    {
      provide: HTTP_INTERCEPTORS,
      useClass: HttpErrorInterceptorService,
      multi: true,
    },
  ],
  bootstrap: [App],
})
export class AppModule {}

/* archivo src/app/app-module.ts */

Crear el componente de listar

En la terminal, ejecute el siguiente comando para generar el componente book-list en la aplicación:

ng generate component book/book-list --type=component

Esto creará una nueva carpeta llamada book-list dentro de src/app/book, donde encontrarás los archivos del componente generados automáticamente (ver Figura 3).

Figura 3. Nuevo componente generado.

Declarar y exportar el componente en el módulo

El resultado es:

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { BookListComponent } from './book-list/book-list.component';

@NgModule({
  declarations: [BookListComponent],
  imports: [CommonModule],
  exports: [BookListComponent],
})
export class BookModule {}

/* archivo src/app/book/book-module.ts*/

Invocar el componente

Para invocar el componente de los libros dentro del HTML del componente principal, tenemos que seguir los siguientes pasos:

  1. Buscamos el nombre del selector del nuevo component BookListComponent. Vamos al archivo book-list.component.ts y en el decorador del componente buscamos el valor del atributo selector. Para este caso el nombre es app-book-list.
  2. Usamos ese selector en el componente principal denominado app.html. Para esto borramos el contenido del archivo app.component.html y agregamos una nueva etiqueta HTML con el valor correspondiente al nombre del nuevo componente, así:
<app-book-list></app-book-list>
<!--archivo src/app/app.component.html -->

Al guardar los cambios y visualizar la página en el navegador obtenemos la salida descrita en la Figura 4. Significa que ya nuestra aplicación está invocando al componente de listar los libros y podemos seguir desarrollandolo.

src/app/book/book.module.ts

Figura 4. Salida del navegador.

El modelo se refiere a la información que será desplegada o ingresada en o por la vista del componente y que será mantenida por el componente de forma sincronizada (binding). Significa que si el modelo cambia la vista se actualiza de forma automática.

En nuestro ejemplo, debemos definir las clases de la estructura de los libros. El diseño de esta estructura, obviamente, depende de la estructura de los objetos JSON que nos va a retornar el API Rest que estamos utilizando. En nuestro caso, los objetos que retorna el API REST al solicitar Get books son los descritos en la Figura 5.

Figura 5. Objetos que retorna el API.

En este ejemplo, solo vamos a crear la representación básica de Book. Es decir, las clases que están encerradas en la línea amarilla: Book y Editorial.

Crear la clase Book

La clase Book la creamos dentro de la carpeta del módulo book. Para crear la clase book ejecutaremos desde la terminal el siguiente comando:

ng generate class book/book --skip-tests true
import { Editorial } from "../editorial/editorial";

export class Book {
 id: number;
 name: string;
 isbn: string;
 description: string;
 image: string;
 publishingDate: any;
 editorial: Editorial;

 constructor(
   id: number,
   name: string,
   isbn: string,
   description: string,
   image: string,
   publishingDate: any,
   editorial: Editorial
 ) {
   this.id = id;
   this.name = name;
   this.isbn = isbn;
   this.description = description;
   this.image = image;
   this.publishingDate = publishingDate;
   this.editorial = editorial;
 }
}
/* archivo src/app/book/book.ts */

Crear la clase Editorial

Al crear la clase Book aparece un error que indica que no se puede encontrar el nombre Editorial; por tanto, debemos crear la clase Editorial. Para hacerlo de forma ordenada, debemos crear el módulo editorial, dado que en nuestro ejemplo este es un módulo funcional distinto.

Creamos el módulo y luego crearemos la clase editorial, para esto debemos ejecutar los siguientes comandos en la línea de código:

ng generate module editorial --type-separator=.
ng generate class editorial/editorial --skip-tests true

Dentro del módulo creamos la clase Editorial, que tiene dos atributos y el correspondiente constructor:

export class Editorial {
 id: number;
 name: string;

 constructor(id: number, name: string) {
   this.id = id;
   this.name = name;
 }
}
/*archivo src/app/editorial/editorial.ts*/

No hay que olvidar asociar el módulo EditorialModule en el módulo principal AppModule quien lo debe importar:

...
import { EditorialModule } from './editorial/editorial.module';

@NgModule({
 ...
  imports: [
   ...
    EditorialModule
  ],
 ...
/*archivo src/app/app.module.ts*/

Luego actualizamos la clase Book incluyendo la referencia a la clase Editorial para corregir el error.

Asociar el modelo con el componente

Ahora que tenemos la clase que representa los libros, podemos declarar, dentro de la clase del componente book-list.component.ts un arreglo para los libros:

books: Array<Book> = []; 

Al incluir esta línea nos aparece que Book no está definido, entonces debemos importarlo:

import { Component, OnInit } from '@angular/core';
import { Book } from '../book';

@Component({
  selector: 'app-book-list',
  standalone: false,
  templateUrl: './book-list.html',
  styleUrl: './book-list.css',
})
export class BookList implements OnInit {
  books: Array<Book> = [];
  constructor() {}

  ngOnInit(): void {}
}

Crear la clase del servicio

Vamos a crear la clase del servicio desde la carpeta book. De nuevo usamos Angular CLI para crear el service de libros. Ejecuta el siguiente comando en la línea de comandos:

ng generate service book/book.service

En la clase BookService inyectamos en el constructor un atributo privado http de tipo HttpClient e importamos el archivo de la clase correspondiente:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';

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

constructor(private http: HttpClient) { }

}

Antes de seguir, vamos al módulo principal AppModule e incluimos en el atributo imports del decorador del módulo, HttpClientModule para que BookService pueda usar HttpClient. El módulo app-module.ts debe quedar de la siguiente forma:

import { NgModule, provideBrowserGlobalErrorListeners } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { AppRoutingModule } from './app-routing-module';
import { App } from './app';
import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http';
import { ToastrModule } from 'ngx-toastr';
import { HttpErrorInterceptorService } from './interceptors/http-error-interceptor.service';
import { BookModule } from './book/book.module';
import { EditorialModule } from './editorial/editorial.module';

@NgModule({
  declarations: [App],
  imports: [
    BrowserModule,
    AppRoutingModule,
    ToastrModule.forRoot({
      timeOut: 10000,
      positionClass: 'toast-bottom-right',
      preventDuplicates: true,
    }),
    BookModule,
    EditorialModule,
    HttpClientModule,
  ],
  providers: [
    {
      provide: HTTP_INTERCEPTORS,
      useClass: HttpErrorInterceptorService,
      multi: true,
    },
  ],
  bootstrap: [App],
})
export class AppModule {}

Configurar la URL del back-end

Para esto se necesita conocer la URL donde está el servidor que provee los libros (el back-end).

El valor de la URL de base donde se encuentra el back-end lo vamos a declarar dentro del archivo environment.development.ts que está en la carpeta environments (ver Figura 6).

Si aún no tiene la carpeta de environments puede generarla con el siguiente comando:

ng generate environments

Figura 6. Carpeta environments.

Luego de ejecutar el backend de ejemplo, este quedará desplegado en la siguiente dirección, que de acá en adelante conoceremos como la URL base (baseUrl):

http://localhost:8080/api/

De este modo, el archivo environment.development.ts quedará así:

export const environment = {
  production: false,
  baseUrl: 'http://localhost:8080/api/',
};

Definición de la función http get en el servicio

En la clase del servicio (book.service.ts) vamos a importar la url del API definida en el archivo environment.development.ts. Para eso agregamos este import:

import { environment } from '../../environments/environment.development';

Luego creamos un atributo de clase denominado apiUrl y lo inicializamos así:

private apiUrl: string = environment.baseUrl + 'books';

En el siguiente paso declaramos una función getBooks() que va a utilizar el servicio http para invocar el http.get. Estas funciones de http retornan objetos Observable, entonces la declaración completa de la función es:

 getBooks(): Observable<Book[]> {
   return this.http.get<Book[]>(this.apiUrl);
 }

Para completar este código debemos importar el archivo de Observable y el archivo de Book.

El código completo del servicio es el siguiente:

/src/app/book/book.service.ts

import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { environment } from '../../environments/environment.development';
import { Observable } from 'rxjs';
import { Book } from './book';

@Injectable({
  providedIn: 'root',
})
export class BookService {
  private apiUrl: string = environment.baseUrl + 'books';
  constructor(private http: HttpClient) {}

  getBooks(): Observable<Book[]> {
    return this.http.get<Book[]>(this.apiUrl);
  }
}

Para completar el componente y que se puedan visualizar los libros, se debe importar el servicio book-service en el componente de book-list.component.ts e invocarlo en el constructor:

...
import { BookService } from '../book.service';
...
constructor(private bookService: BookService) {}
...

Adicionalmente, se debe declarar el método getBooks(). Este método usa el servicio book.service para llamar al método getBooks() dentro de la clase del servicio con el fin de suscribirse y asignar el valor de esa lista al Array de books definido en el componente:

...  
 getBooks(): void {
    this.bookService.getBooks().subscribe((books) => {
      this.books = books;
    });
  }
...

Finalmente, se debe llamar al método getBooks() dentro del método ngOnInit() que se llama cuando se inicia el componente:

...  
 ngOnInit(): void {
    this.getBooks();
  }

Por lo tanto, el código del componente queda de la siguiente manera:

import { Component, OnInit } from '@angular/core';
import { Book } from '../book';
import { BookService } from '../book.service';

@Component({
  selector: 'app-book-list',
  standalone: false,
  templateUrl: './book-list.component.html',
  styleUrl: './book-list.component.css',
})
export class BookListComponent implements OnInit {
  books: Array<Book> = [];

  constructor(private bookService: BookService) {}

  getBooks(): void {
    this.bookService.getBooks().subscribe((books) => {
      this.books = books;
    });
  }

  ngOnInit(): void {
    this.getBooks();
  }
}

Como ya hemos explicado, la vista es el HTML asociado con el componente. El objetivo es desplegar la lista de libros en una galería con las imágenes de las portadas de los libros.

Veamos el código de la vista (archivo book-list.component.html), empezando solo con el HTML básico y el uso de Bootstrap. En el siguiente código HTML, tenemos un container-fluid, una fila y una columna con un margen en la parte inferior.

<div class="container-fluid">
  <div class="row">
      <div class="col mb-2"></div>
  </div>
</div>

Queremos crear una columna por cada libro que se encuentra en el arreglo books. Entonces, necesitamos definir un ciclo sobre la etiqueta div que define la columna para que itere sobre books y cree una nueva columna en cada iteración. También vamos a mostrar el nombre del libro en cada columna utilizando la expresión {{book.name}}

<div class="container-fluid">
  <div class="row">
    @for (book of books; track book.id) {
    <div class="col mb-2">{{ book.name }}</div>
    }
  </div>
</div>

Ahora vamos a incluir la imagen del libro. Hay muchas formas de hacerlo, utilizando etiquetas como figure o como card.

Vamos a crear una etiqueta card al interior de cada columna. Como se observa, se configura un padding (con la clase p-2) y un ancho y alto a cada card. Adicionalmente, dentro de card tenemos la etiqueta para la imagen, el atributo src donde define la URL de la imagen, el atributo alt contiene el texto alternativo que describe la imagen y que será mostrado cuando la imagen no pueda cargar. Finalmente, se tiene una etiqueta h5 y una etiqueta p las cuales hacen parte del cuerpo de card y sirven para mostrar el nombre del libro y su editorial respectivamente:

<div class="container-fluid">
  <div class="row">
    @for (book of books; track book.id) {
    <div class="card p-2" style="width: 15rem; height: 33rem">
      <img class="card-img-top" src="{{ book.image }}" alt="{{ book.name }}" />
      <div class="card-body">
        <h5 class="card-title">{{ book.name }}</h5>
        <p class="card-text">[{{ book.editorial.name }}]</p>
      </div>
    </div>
    }
  </div>
</div>

Dado que las imágenes en el back pueden tener tamaños diferentes y esto podría afectar la visualización vamos a incluir el siguiente código en el archivo book-list.component.css que deja un tamaño fijo para cada imagen:

img {
 height: 350px;
 width: auto;
 max-width: 400px;
}

Después de ejecutar el back, el nuevo despliegue es el que se observa en la Figura 7.

Figura 7. Aplicación final.