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 BokkListarComponent
para desplegar el catálogo de libros que está en la aplicación del curso backstepbystep.
Los pasos que siguen son:
El resultado final del tutorial es una aplicación que despliega la siguiente lista (Figura 1):
Figura 1
Antes de realizar este tutorial Ud. ya desarrolló el tutorial Lista de cursos. Allí se explican varios conceptos que no se retoman aquí.
En particular Ud. debe:
Para ejecutar el resultado final de este taller Ud. debe tener en ejecución sobre payara, el proyecto backstepbystep. No olvide inicializar la base de datos y ejecutar el sql que inserta los datos.
Cree una aplicación Angular que se llame, por ejemplo, book
, siguiendo las instrucciones que se encuentran aquí.
Abra su aplicación en VSCode
borre el contenido del archivo app.component.html.
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
.
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 { }
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:
imports
el nombre del módulo, es decir, de la clase BookModule
import { BookModule } from './book/book.module';
Vaya sobre la carpeta del módulo src/app/book
, 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
:
BookModule
(book.module.ts
) y agregue en el decorador, en el atributo declarations, el nombre del nuevo componente BookListarModule
. El resultado es:
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 { }
Para invocar el componente de los libros dentro del HTML del componente principal, tenemos que seguir los siguientes pasos:
BookListarComponent
. Vamos al archivo book-listar-component.ts
y en del decorador del componente buscamos el valor del atributo selector
.app.component.html.
Escríbalo en la segunda línea.Volvemos a ejecutar la aplicación y obtenemos. Significa que ya nuestra aplicación está invocando al componente de listar los libros y podemos seguir desarrollandolo.
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:
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
.
La clase Book la creamos dentro de la carpeta del módulo book. Esta clase define los atributos y sus métodos getters.
import { Editorial } from '../editorial/editorial';
export class Book {
constructor(private idA: number, private nameA: string, private isbnA: string,
private descriptionA: string, private imageA: string, private publishingdateA: any,
private editorialA: Editorial) { }
get id(): number { return this.idA; }
get name(): string { return this.nameA; }
get isbn(): string { return this.isbnA; }
get description(): string { return this.descriptionA; }
get image(): string { return this.imageA; }
get publishingdate(): any { return this.publishingdateA; }
get editorial(): Editorial { return this.editorialA; }
}
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 los correspondientes getters:
export class Editorial {
constructor(private idA: number, private nameA: string) {
}
get id(): number { return this.idA; }
get name(): string { return this.nameA; }
}
Ahora que tenemos la clase que representa los libros, podemos declarar, dentro de la clase del componente BookListarComponent, un arreglo para los libros:
private 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() { }
private books: Array<Book>;
ngOnInit() {
}
}
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 { }
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
:
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}/`
};
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:
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 = environment.baseUrl + 'books';
constructor(private http: HttpClient) { }
getBooks(): Observable<Book[]> {
return this.http.get<Book[]>(this.apiUrl);
}
}
Nuestro componente debe ahora llamar la función creada en el servicio. Tenemos que hacer varias cosas:
Observable
.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
para que cuando se cree el componente de listar, se llame el back-end para traer los datos de los libros. ngOnInit() {
this.getBooks();
}
El código completo del componente se puede ver aquí:
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) { }
private 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:
b
, y books
que debe estar obligatoriamente definida en la clase del componente (en este caso, BookListarComponent
).class
de row una indicación para dejar un margen de 3 pixeles en los cuatro lados y un border
en las columnas solo para apreciarlas mejor. El resultado se muestra en la siguiente imagen donde aparece en cada columna el nombre del libro. En la colección que obtenemos solo hay definidos 6 libros. Ahora vamos a incluir la imagen del libro. Hay muchas formas de hacerlo, utilizando etiquetas cono 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::