¿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 BookListComponent 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?

Antes de realizar este tutorial Ud. ya desarrolló el tutorial inicial. Allí se explican varios conceptos que no se retoman aquí.

En particular Ud. debe:

  1. Tener instalado el ambiente: VSCode, TS, Angular.
  2. Saber cómo se instala Bootstrap en el proyecto.
  3. Saber cómo se crean módulos, componentes, servicios utilizando angular-cli en VSCode.

Crear el proyecto

Cree una aplicación Angular que se llame, por ejemplo, book, siguiendo las instrucciones que se encuentran en el tutorial de creación de aplicaciones en Angular.

Abra su aplicación en VSCode borre el contenido del archivo app.component.html.

Crear un nuevo módulo

Para crear el nuevo módulo utilizamos la aplicación angular-cli que está integrada dentro de VSCode.

Para esto, vaya a la carpeta src/app, clic derecho, Generate Module. El nombre del nuevo módulo es book.

Borre el componente por defecto

Al crear el nuevo módulo book, se generó un componente por defecto BookComponent.

Para eliminar ese componente que no vamos a utilizar debe borrar los siguientes archivos:

book.component.ts

book.component.css

book.component.html

Borre referencias a este componente en el archivo book.module.ts que debe quedar así:

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
@NgModule({
  imports: [
    CommonModule
  ],
  declarations: []
})
export class BookModule { }

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 (archivo app.module.ts).

Para importar en el módulo principal el módulo de BookModule se debe realizar dos cosas:

import { BookModule } from './book/book.module';

Crear el componente de listar

En la carpeta del módulo src/app/book, haga clic derecho Generate Component y escriba por nombre book-listar.

Dentro de la carpeta src/app/book se debió crear una nueva carpeta para el componente book-listar (ver Figura 2).

Figura 2. Nuevo componente generado.

Declarar y exportar el componente en el módulo

El resultado es:

src/app/book/book.module.ts

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { BookListarComponent } from './book-listar/book-listar.component';
@NgModule({
  imports: [
    CommonModule
  ],
  declarations: [BookListarComponent],
  exports: [BookListarComponent],
})
export class BookModule { }

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 de BookListarComponent. Vamos al archivo book-listar-component.ts y en el decorador del componente buscamos el valor del atributo selector.
  2. Lo utilizamos en app.component.html. Escríbalo en la segunda línea (ver Figura 3).

Figura 3. Inclusión del selector del componente en el componente principal.

Volvemos a ejecutar la aplicación y 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.

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 es el descrito en la Figura 5.

Figura 5. Objetos que retorna el API.

En este ejemplo, solo vamos a representar 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. Esta clase define los atributos y su constructor.

src/app/book/book.ts

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;
 }
}

Crear la clase Editorial

Para crear la clase Editorial, de forma ordenada, debemos crear el módulo EditorialModule, dado que en nuestro ejemplo este es un módulo funcional distinto.

Creamos el módulo, utilizando el angular-cli, debe quedar en su propia carpeta así:

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
  ],
 ...

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;
 }
}

Asociar el modelo con el componente

Ahora que tenemos la clase que representa los libros, podemos declarar, dentro de la clase del componente BookListarComponent, un arreglo para los libros:

books: Array<Book>; 

Nos aparece que Book no está definido, entonces debemos importarlo:

import { Component, OnInit } from '@angular/core';
import { Book } from '../book';
@Component({
  selector: 'app-book-listar',
  templateUrl: './book-listar.component.html',
  styleUrls: ['./book-listar.component.css']
})
export class BookListarComponent implements OnInit {
  constructor() { }
  books: Array<Book>;
  ngOnInit() {
  }
}

Crear la clase del servicio

Vamos a crear la clase del servicio desde la carpeta book. De nuevo usamos angular-cli y seleccionado Generate Service. Como nombre, solo escribimos book ya que angular-cli completa con la palabra Service.

El archivo generado resultado es:

import { Injectable } from '@angular/core';

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

En BookService creamos un atributo privado http de tipo HttpClient de Angular.

Inyectamos ese servicio declarando en el constructor e importando el archivo de la clase correspondiente:

import { HttpClient } from '@angular/common/http';
...
export class BookService {
    constructor(private http: HttpClient) { }
...
}

Antes de seguir, vamos al módulo principal AppModule e importamos (en el atributo imports del decorador del módulo, HttpClientModule para que BookServicio pueda usar el HttpClient. El módulo debe quedar de la siguiente forma:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { HttpClientModule } from '@angular/common/http';

import { AppComponent } from './app.component';
import { BookModule } from './book/book.module';
import { EditorialModule } from './editorial/editorial.module';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    BookModule,
    EditorialModule,
    HttpClientModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
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.ts que está en la carpeta environments (ver Figura 6).

Figura 6. Carpeta environments.

Definimos la url del back-end, suponiendo que el back-end está en el localhost y que se ejecuta en el 8080:

const host = 'localhost';
const port = '8080';
const appName = 'frontstepbystep-api';
const rootApi = 'api';

export const environment = {
  production: false,
baseUrl:`http://${host}:${port}/${appName}/${rootApi}/`
};

Para facilitar la ejecución de este ejemplo, tenemos el back-end ejecutándose en una IP de producción. Entonces los valores son:

const host = '157.253.238.75';
const port = '8084';
const appName = 'frontstepbystep-api';
const rootApi = 'api';

export const environment = {
  production: false,
  baseUrl: `http://${host}:${port}/${appName}/${rootApi}/`
};

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

En la clase del servicio, declaramos una función getBooks() que va a utilizar el servicio http para invocar el http.get. Como ya explicamos, 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 { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { Book } from './book';
import { environment } from '../../environments/environment';
 

@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);
  }
}

Siguiendo los pasos establecidos en el tutorial sobre pruebas unitarias, se realiza la prueba del servicio, particularmente para el método getBooks.

El código a continuación describe el contenido de la prueba.

/* tslint:disable:no-unused-variable */

import { TestBed, async, inject, getTestBed } from "@angular/core/testing";
import { BookService } from "./book.service";

import {
 HttpTestingController,
 HttpClientTestingModule,
} from "@angular/common/http/testing";

import faker from "faker";
import { Book } from "./book";
import { environment } from "../../environments/environment";

describe("Service: Book", () => {
 let injector: TestBed;
 let service: BookService;
 let httpMock: HttpTestingController;
 let apiUrl = environment.baseUrl + "books";

 beforeEach(() => {
   TestBed.configureTestingModule({
     imports: [HttpClientTestingModule],
     providers: [BookService],
   });
   injector = getTestBed();
   service = injector.get(BookService);
   httpMock = injector.get(HttpTestingController);
 });

 afterEach(() => {
   httpMock.verify();
 });

 it("getPost() should return 10 records", () => {
   let mockPosts: Book[] = [];

   for (let i = 1; i < 11; i++) {
     let book = new Book(
       i,
       faker.lorem.sentence(),
       faker.random.number(),
       faker.lorem.sentence(),
       faker.image.imageUrl(),
       faker.date.past(),
       null
     );

     mockPosts.push(book);
   }

   service.getBooks().subscribe((books) => {
     expect(books.length).toBe(10);
   });

   const req = httpMock.expectOne(apiUrl);
   expect(req.request.method).toBe("GET");
   req.flush(mockPosts);
 });
});

Nuestro componente debe ahora llamar la función creada en el servicio. Tenemos que hacer varias cosas:

Para poder usar el servicio en el componente necesitamos declararlo en el constructor e importar el archivo, el fragmento de código donde está esta declaración es:

...
import { BookService } from '../book.service';

@Component({
  selector: 'app-book-listar',
  templateUrl: './book-listar.component.html',
  styleUrls: ['./book-listar.component.css']
})
export class BookListarComponent implements OnInit {
  constructor(private bookService: BookService) { }
...
}

Debemos declarar el método del componente getBooks() así:

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

El código completo del componente se puede ver aquí:

/src/app/book/book-listar.component.ts

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

@Component({
  selector: 'app-book-listar',
  templateUrl: './book-listar.component.html',
  styleUrls: ['./book-listar.component.css']
})
export class BookListarComponent implements OnInit {
  constructor(private bookService: BookService) { }

  books: Array<Book>;

  getBooks(): void {
    this.bookService.getBooks()
      .subscribe(books => {
        this.books = books;
      });
  }
  ngOnInit() {
    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.

Vamos a hacer primero un despliegue muy básico de las imágenes de los libros utilizando la grilla de Bootstrap. Cada imagen irá en una columna y debajo de la imagen va el nombre del libro. Las imágenes son responsive y todo está dentro de un container-fluid.

Antes de hacer la vista es importante que instale la dependencia de bootstrap (npm install bootstrap), y haber incluido en el archivo angular.json, en el atributo styles -línea 22-, el siguiente valor: "node_modules/bootstrap/dist/css/bootstrap.min.css"

Ahora veamos el código de la vista, 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.

<div class="container-fluid">
  <div class="row">
      <div class="col">

      </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, vamos a escribir el nombre del libro en cada columna utilizando la expresión {{b.name}}:

<div class="container-fluid">
  <div class="row p-5">
      <div class="col border" *ngFor= "let b of books">
         {{b.name}}
      </div>
  </div>
</div>

Repasemos la sintaxis para definir un ciclo dentro del HTML utilizando las directivas de Angular:

*ngFor= "let b of books"

La directiva *ngFor es un atributo que se define en la etiqueta donde queremos iniciar el ciclo. El ciclo se termina donde se termina esa etiqueta. El valor del atributo debe definir:

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 figure al interior de cada columna. Dentro de figure tenemos la imagen, etiqueta img, y el título de la imagen, etiqueta figcaption, que en este caso es el nombre del libro. Hemos, además, agregado el nombre de la editorial.

<div class="container-fluid">
  <div class="row p-5">
      <div class="col" *ngFor="let b of books">
          <figure class="figure">
              <img class="img-fluid" src='{{b.image}}'/>
              <figcaption class="title-caption text-center">{{b.name}}. [{{b.editorial.name}}]</figcaption>
          </figure>
      </div>
  </div>
</div>

El nuevo despliegue es el que se observa en la Figura 7.

Figura 7. Aplicación final.

En el siguiente código se está realizando la prueba del componente. Para esto se crea un nuevo libro con unos datos ficticios proporcionados por la librería faker.js. En la prueba se verifica que exista un elemento de tipo figure que tenga como valor tanto el nombre del nuevo libro creado como el nombre de la editorial.

/* tslint:disable:no-unused-variable */
import { async, ComponentFixture, TestBed } from "@angular/core/testing";
import { By } from "@angular/platform-browser";
import { DebugElement } from "@angular/core";

import { BookListarComponent } from "./book-listar.component";
import { HttpClientTestingModule } from "@angular/common/http/testing";
import faker from "faker";
import { Book } from "../book";
import { Editorial } from "src/app/editorial/editorial";

describe("BookListarComponent", () => {
 let component: BookListarComponent;
 let fixture: ComponentFixture<BookListarComponent>;
 let debug: DebugElement;

 beforeEach(async(() => {
   TestBed.configureTestingModule({
     declarations: [BookListarComponent],
     imports: [HttpClientTestingModule],
   }).compileComponents();
 }));

 beforeEach(() => {
   fixture = TestBed.createComponent(BookListarComponent);
   component = fixture.componentInstance;
   let editorial = new Editorial(
     faker.random.number(),
     faker.lorem.sentence()
   );
   component.books = [
     new Book(
       faker.random.number(),
       faker.lorem.sentence(),
       faker.random.number(),
       faker.lorem.sentence(),
       faker.image.imageUrl(),
       faker.date.past(),
       editorial
     ),
   ];
   fixture.detectChanges();
   debug = fixture.debugElement;
 });

 it("should create", () => {
   expect(component).toBeTruthy();
 });

 it("Should have an figcaption element ", () => {
   expect(debug.query(By.css("figcaption")).nativeElement.innerText).toContain(
     component.books[0].name
   );

   expect(debug.query(By.css("figcaption")).nativeElement.innerText).toContain(
     component.books[0].editorial.name
   );
 });
});

Para ejecutar la prueba, en una consola digite ng test, y debe ver la salida proporcionada en la FIgura 8.

Figura 8. Salida de la ejecución de la prueba.