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.
Una aplicación que incluye un componente que se ocupa de desplegar el detalle de un libro. También 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. |
Antes de realizar este tutorial usted ya desarrolló el tutorial de desplegar lista de libros. Este taller es una extensión.
En particular usted debe:
Este tutorial es una extensión del tutorial de listar usted puede realizarlo sobre su versión del tutorial anterior o hacer un fork del proyecto de BookList para extender ese código.
Una vez que tenga el nuevo proyecto:
node_modules en el nivel del root, borrela.code .npm install desde la carpeta principal del proyectong serve y verifique que puede ver la lista de los libros, tal como se observa en la Figura 2.
|
Figura 2. Listado de los libros. |
Para crear el componente book-detail, utilizaremos la línea de comandos de Angular CLI, lo cual garantiza que se integre correctamente al módulo del proyecto.
ng generate component book/book-detail --type=component
Esto creará una nueva carpeta llamada book-detail dentro de src/app/book, donde encontrarás los archivos del componente generados automáticamente (ver Figura 3).
|
Figura 3. Carpeta y archivos el nuevo componente. |
BookModule (book.module.ts) y asegúrese que el decorador en el atributo declarations, tiene el nombre del nuevo componente BookDetailComponent. El resultado es:
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { BookListComponent } from './book-list/book-list.component';
import { BookDetailComponent } from './book-detail/book-detail.component';
@NgModule({
declarations: [BookListComponent, BookDetailComponent],
imports: [CommonModule],
exports: [BookListComponent],
})
export class BookModule {}
Ejecute el proyecto con ng serve y verifique que no haya 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 decorándolo 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.
Se incluye el signo de admiración de cierre en el nombre del atributo para evitar que tenga un valor indefinido por defecto.
import { Component, Input, OnInit } from '@angular/core';
import { Book } from '../book';
@Component({
selector: 'app-book-detail',
standalone: false,
templateUrl: './book-detail.component.html',
styleUrl: './book-detail.component.css',
})
export class BookDetailComponent implements OnInit {
@Input() bookDetail!: Book;
constructor() {}
ngOnInit(): void {}
}
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 la imagen y en otra 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 }}" alt="{{ bookDetail.name }}" />
</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>{{ bookDetail.publishingDate }}</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>
En la vista (Html) del componente de listar (book-list.html) desplegamos el componente detalle utilizando su selector:
<app-book-detail></app-book-detail>
Si está ejecutando el servidor mientras hace la corrección, la terminal mostrará un error como se muestra a continuación.

Momentáneamente ignoraremos este error.
Sin embargo, queremos que aparezca el detalle solamente cuando un libro ha sido seleccionado. Para esto utilizamos la directiva de Angular @if. Esta directiva nos permite hacer un condicional sobre el HTML; esto significa que si la condición se cumple, se muestra lo que está dentro del condicional que contiene el @if.
@if (selected) {
<app-book-detail></app-book-detail>
}
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 de código:
...
export class BookListComponent implements OnInit {
...
selected: Boolean = false;
...
Inicializamos la variable selected en false y luego cuando el usuario selecciona un libro, le cambiamos el valor a true.
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='{{book.image}}' alt="{{book.name}}"
(click)="onSelected(book)" />
La función debe definirse en el componente y se ocupa de:
selectedonSelected: export class BookListComponent implements OnInit {
...
selectedBook!: Book;
selected = false;
...
onSelected(book: Book): void {
this.selected = true;
this.selectedBook = book;
}
...
}
Ya tenemos el libro que debemos enviar al componente detalle para que lo muestre. Ahora tenemos que asociar ese libro con el atributo @Input del componente del detalle. Para esto incluimos esto en la vista de listar:
@if (selected) {
<app-book-detail [bookDetail]="selectedBook"></app-book-detail>
}
Note que 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 selecciona (ver Figura 4).
|
Figura 4. Paso de parámetros entre componentes. |
Hasta el momento este es el código de la vista componente book-list:
@if (selected) {
<app-book-detail [bookDetail]="selectedBook"></app-book-detail>
}
<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 }}"
(click)="onSelected(book)"
/>
<div class="card-body">
<h5 class="card-title">{{ book.name }}</h5>
<p class="card-text">[{{ book.editorial.name }}]</p>
</div>
</div>
}
</div>
</div>
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 el detalle de Book con 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:
Book, los atributos básicos y la relación con Editorial que es de cardinalidad 1. authors que corresponde a los atributos básicos de los autores de los librosreviews que corresponde a los atributos de las reseñas de los libros.BookList tiene una colección books de tipo BookDetail y que BookDetailComponente tiene una referencia a la clase BookDetail.Vamos a completar el ejemplo creando los siguientes elementos en su proyecto:
BookDetail , momentáneamente quedará vacía.Author, esto implica crear un nuevo módulo author para guardar la nueva clase.Review. Esta clase la vamos a definir dentro del módulo book dado que la relación entre Book y Review es composite (Review depende de Book).Adicionalmente, en BookList cambiamos el tipo de dato de la variable selectedBook de Book a BookDetail como se ve a continuación:
books: Array<Book> = [];
selectedBook!: Book;
onSelected(book: Book): void {
this.selected = true;
this.selectedBook = book;
}
por:
books: Array<BookDetail> = [];
selectedBook!: BookDetail;
onSelected(book: BookDetail): void {
this.selected = true;
this.selectedBook = book;
}
Y en BookDetailComponent tambien cambiamos el tipo de la variable bookDetail como se muestra a continuación:
@Input() bookDetail!: Book;
por:
@Input() bookDetail!: BookDetail;
La siguiente es la definición de la clase BookDetail. Esta clase se incluye en el módulo book. Note que utiliza la clase Review y la clase Author para representar las colecciones.
import { Author } from '../author/author';
import { Book } from './book';
import { Editorial } from '../editorial/editorial';
import { Review } from './review';
export class BookDetail extends Book {
authors: Array<Author> = [];
reviews: Array<Review> = [];
constructor(
id: number,
name: string,
isbn: string,
description: string,
image: string,
publishingDate: any,
editorial: Editorial,
authors: Array<Author>,
reviews: Array<Review>
) {
super(id, name, isbn, description, image, publishingDate, editorial);
this.authors = authors;
this.reviews = reviews;
}
}
La siguiente es la definición de la clase Review la cual estará dentro del módulo book:
export class Review {
id: number;
source: string;
name: string;
description: string;
constructor(
id: number,
source: string,
name: string,
description: string
) {
this.id = id;
this.source = source
this.name = name;
this.description = description;
}
}
La clase Author debe estar dentro de un nuevo módulo denominado author. Cree el módulo, remueva el componente que se crea por defecto, elimine la mención al componente en el módulo y cree la clase author. La siguiente es la definición de la clase Author.
export class Author {
id: number;
name: string;
birthDate: any;
image: string;
description: string;
constructor(id: number, name: string, birthDate: any, image: string, description: string) {
this.id = id;
this.name = name;
this.birthDate = birthDate;
this.image = image;
this.description = description;
}
}
En el servicio BookService cambiamos el método para traer los libros teniendo en cuenta el nuevo tipo de dato Detail:
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { environment } from '../../environments/environment.development';
import { Observable } from 'rxjs';
import { BookDetail } from './book-detail';
@Injectable({
providedIn: 'root',
})
export class BookService {
private apiUrl: string = environment.baseUrl + 'books';
constructor(private http: HttpClient) {}
getBooks(): Observable<BookDetail[]> {
return this.http.get<BookDetail[]>(this.apiUrl);
}
}
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>
@for(author of bookDetail.authors; track author.id) {
<dd class="caption">{{ author.name }}</dd>
}
|
Figura 7. Resultado final de la aplicación. |
Como ejercicio se propone modificar la vista del componente de detalle para mostrar las reviews de un libro. En la prueba del componente incluya un nuevo spec para verificar que las reviews se muestran correctamente.