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. 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. |
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:
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/
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:
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. |
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. |
BookModule
(book.module.ts
) y agregue en el decorador, en el atributo declarations, el nombre del nuevo componente BookDetailModule
. El resultado es:
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>
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
.
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:
selected
.onSelected
: export class BookListarComponent implements OnInit {
...
selectedBook: Book;
selected = false;
...
onSelected(b: Book): void {
this.selected = true;
this.selectedBook = b;
}
...
}
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. |
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
:
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 reviews de los libros.BookListComponent
tiene una colección books
de tipo BookDetail
y que BookDetailComponent
Vamos a completar el ejemplo con:
BookDetail
Author
(en un nuevo módulo author
)Review
. Esta clase la vamos a definir dentro del módulo book
dado que la relación entre Book
y Review
es un composite (Review
depende de Book
).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. |
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);
}
}
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. |