¿Qué aprenderá?

Este tutorial lo guía, usando VSCode, en la construcción de una aplicación Angular que permite desplegar una lista de libros, junto con los detalles de cada libro usando el patrón maestro / detalle.

¿Qué construirá?

Una aplicación que incluye un componente que se ocupa de desplegar el detalle de un libro. Tambień definirá la navegación para que cuando el usuario haga clic en una imagen del catálogo de libros, se muestre el detalle de ese libro.

El resultado final del tutorial es una aplicación que despliega la lista detallada en la Figura 1.

Figura 1. Aplicación que desarrollará en este tutorial.

¿Qué necesita?

Antes de realizar este tutorial Ud. ya desarrolló el tutorial de desplegar lista de libros. Este taller es una extensión.

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 el angular-cli en VSCode.

Este tutorial usa un back existente correspondiente al proyecto Backstepbystep. Una instancia de ese back se ejecuta en la siguiente URL:

http://157.253.238.75:8084/frontstepbystep-api/api/

Clonar el proyecto

Este tutorial es una extensión del tutorial de listar. Ud puede realizarlo sobre su versión del tutorial anterior o hacer un fork del proyecto de bookListar para extender ese código.

Una vez que tenga el nuevo proyecto:

Figura 2. Listado de los libros.

Crear el componente detalle

Vaya sobre la carpeta del módulo src/app/book, clic derecho Generate Component y escriba por nombre book-detail.

Dentro de la carpeta src/app/book se debió crear una nueva carpeta para el componente book-detail, tal como se ve en la Figura 3.

Figura 3. Carpeta y archivos el nuevo componente.

Declarar 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';
import { BookDetailComponent } from './book-detail/book-detail.component';

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

Ejecute el proyecto con ng serve y verifique que no hay ningún problema.

En esta solución el componente detalle recibe de parámetro, como entrada, el libro que va a desplegar. Esto se define declarando el atributo cuyo valor se va a recibir y decorandolo con @Input().

El nombre que le hemos dado al atributo es bookDetail. Este es el que se debe utilizar en el template de la vista,.

@Component({
  selector: 'app-book-detail',
  templateUrl: './book-detail.component.html',
  styleUrls: ['./book-detail.component.css']
})
export class BookDetailComponent implements OnInit {

  @Input() bookDetail: Book;

  constructor() { }

  ngOnInit() {
    console.log(this.bookDetail.id);
  }
}

La vista del componente detalle, se ocupa de desplegar los valores del objeto bookDetail. Es importante notar que esta vista "no sabe" dónde la van a desplegar:

Desplegamos el título del libro y luego en una fila con una columna para la imagen y otra la los demás atributos.

<div class="container-fluid">
  <p class="h3 p-3">{{bookDetail.name}}</p>
  <div class="row">
    <div class="col-2">
      <div class="thumb">
        <img class="img-fluid" src="{{bookDetail.image}}" />
      </div>
    </div>
    <div class="col">
      <dl>
        <dt> Authors</dt>
        <dd>No available information</dd>
        <dt> ISBN</dt>
        <dd>{{bookDetail.isbn}}</dd>
        <hr>
        <dt>Publishing Date</dt>
        <dd>{{ strToDate(bookDetail.publishingdate) |date:"longDate"}}</dd>
        <hr>
        <dt class="bold">Editorial</dt>
        <dd class="caption">{{bookDetail.editorial.name}}</dd>
        <hr>
        <dt class="bold">Description</dt>
        <dd>{{bookDetail.description}}</dd>
      </dl>
    </div>
  </div>
</div>

Mostrar el componente detalle solo si hay un elemento seleccionado

En la vista (Html) del componente listar desplegamos el componente detalle utilizando su selector:

<app-book-detail></app-book-detail>

Sin embargo, queremos que aparezca el detalle sí y solo sí, ha sido seleccionado un elemento. Para esto utilizamos la directiva de Angular *ngIf. Esta directiva nos permite hacer un condicional sobre el HTML. SIgnifica que si la condición se cumple, se muestra lo que está dentro de la etiqueta que contiene el *ngIf. veamos:

<div *ngIf="selected">
      <app-book-detail></app-book-detail>
</div>

Si el valor de la variable selected es true entonces se desplegará el componente.

La variable selected debe ser declarada en el componente de listar. Veamos el fragmento:

...
export class BookListarComponent implements OnInit {
...
  selected = false;
...

Inicializamos la variable selected en false y luego cuando el usuario selecciona un libro, le cambiamos el valor a true.

Responder a la selección del usuario

Cuando el usuario selecciona un libro hace clic sobre la imagen, entonces, definimos sobre la etiqueta de la imagen, la invocación a una función en respuesta a ese click:

<img class="img-fluid" src='{{b.image}}' (click)="onSelected(b)" />

La función debe definirse en el componente y se ocupa de:

 export class BookListarComponent implements OnInit {
...
  selectedBook: Book;
  selected = false;
...

  onSelected(b: Book): void {
    this.selected = true;
    this.selectedBook = b;
  }
...
}

Enviar el parámetro input al componente detalle

Ya tenemos el libro que debemos enviar al componente detalle para que lo muestre, para enviarselo al componente y que este lo asocie con el atributo @Input, definimos:

<div *ngIf="selected">
   <app-book-detail [bookDetail]="selectedBook"></app-book-detail>
 </div>

bookDetail es el nombre del atributo anotado con @Input en el componente detalle y selectedBook es es atributo en el componente listar que guarda el libro que el usuario seleccionó (ver Figura 4).

Figura 4. Paso de parámetros entre componentes.

Cambiar el modelo de datos

En el componente de listar libros, definimos el modelo de datos solo con la información básica de Book y su relación de cardinalidad 1 con Editorial. Si queremos desplegar en el detalle de Book los autores, debemos incluir esa información en el modelo.

En el API Rest de nuestra aplicación, todos los servicios GET retornan objetos DetailDTO, es decir, los atributos básicos del objeto y las colecciones de objetos básicos que representan las asociaciones. El diagrama de la Figura 5 muestra el caso para Book.

Figura 5. Diagrama de clases.

Tenemos que BookDetail:

Vamos a completar el ejemplo con:

Adicionalmente, en BookListComponent cambiamos:

books: Array<Book>;

por:

books: Array<BookDetail>;

Y en BookDetailComponent cambiamos:

@Input() bookDetail: Book;

por:

@Input() bookDetail: BookDetail;

La Figura 6 presenta la definición de la clase BookDetail. Note que utiliza la clase Review y la clase Author para representar las colecciones.

Figura 6. Definición de la clase BookDetail.

Cambiar el servicio para recuperar el objeto BookDetail

En el servicio, BookService, cambiamos el método para traer los libros teniendo en cuenta el tipo de dato Detail:

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

  private apiUrl = environment.baseUrl + 'books';
  constructor(private http: HttpClient) { }

  getBooks(): Observable<Array<BookDetail>> {
     return this.http.get<Array<BookDetail>>(this.apiUrl);
  }

}

Paso 3: Cambiar la vista del componente detalle

Para agregar la información de los autores, realizamos un ciclo en la vista HTML del componente detalle. El ciclo itera sobre la colección de autores que se encuentra en el objeto bookDetail.

...

<dt> Authors</dt>

<dd *ngFor = "let a of bookDetail.authors">{{a.name}}</dd>

...

El resultado final se muestra en la Figura 7.

Figura 7. Resultado final de la aplicación.