Este tutorial es una extensión al presentado en Desplegar lista de libros. Extendemos la aplicación para:
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 Desplegar lista de libros. Este taller es una extensión.
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.
Este tutorial es una extensión de bookListar. 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:
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
:
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ó.
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
(ver modelo). 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 siguiente diagrama muestra el caso para Book
:
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 siguiente presenta la definición de la clase BookDetail
. Note que utiliza la clase Review
y la clase Author
para representar las colecciones.
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 imagen: