¿Qué aprenderá?

En este tutorial, podrá seguir los pasos para implementar el manejo de rutas para navegar a través de los componentes de una aplicación Angular.

¿Qué construirá?

La Figura 1 muestra lo que lograremos al finalizar el tutorial. En resumen, a la aplicación se le incorpora un menú de navegación y cuando se seleccionan los elementos de ese menú la parte central de la página cambiará.

Figura 1. Aplicación que se construirá en este tutorial.

¿Qué necesita?

Este tutorial lo hacemos como una extensión al presentado en el de despliegue del detalle de un libro.

Una ruta define un camino (path) que lleva a que un componente se muestre en una zona específica de la página (router-outlet).

En Angular la asociación de una ruta con un elemento de interfaz se define utilizando el atributo routerLink. Este atributo reemplaza al atributo href de un enlace estándar en HTML.

En este tutorial vamos a crear las siguientes rutas y su navegación:

Rutas para listar

Un menú principal en la aplicación que lleva, a través de las rutas, al componente listar de Book, Author y Editorial. Las rutas son:

Elemento donde está asociada la ruta con routerLink

Ruta

Componente

Menú Book

‘books/list'

BookListComponent

Menú Author

‘authors/list'

AuthorListComponent

Menú Editorial

‘editorials/list'

EditorialListComponent

La aplicación fue diseñada para que la zona de despliegue (router-outlet) esté entre el menú y el footer de la aplicación.

Rutas para navegar a detalles

Adicionalmente a la navegación entre listas también se tiene la navegación hacia los detalles. Por ejemplo, en el tutorial de detalle, cuando se hace clic en un libro se muestran sus detalles. En la información de ese detalle tenemos la mención a los autores del libro tal como se ve en la siguiente imagen:

Con la navegación a los detalles el usuario podrá dar clic sobre el nombre del autor. En el nombre se incluye un routerLink con la ruta author/:id

' donde el id corresponde al identificador del autor seleccionado. Esta ruta tiene asociado el componente de detalle del autor denominado AuthorDetailComponent.

Igualmente cuando desde el detalle de autor o desde el detalle de editorial, el usuario da clic sobre la imagen de un libro, esta imagen tiene tiene un routerLink con la ruta book/:id' donde el id corresponde con el identificador del libro seleccionado. Esta ruta tiene asociado el componente de detalle del libro denominado BookDetailComponent.

Elemento donde está asociada la ruta con routerLink

Ruta

Componente

Desde la imagen de un libro

‘books/:id'

BookDetailComponent

Desde el nombre de un autor

‘author/:id

AuthorDetailComponent

La Figura 2 muestra el diagrama de clases de los elementos que intervienen en la relación entre la lista y los detalles.

Figura 2. Diagrama de clases.

AutorModule

En el módulo AuthorModule estarán los componentes AuthorListComponent y AuthorDetailComponent. Para ver el contenido de estos componentes revise el repositorio de GitHub con el ejemplo.

El componente de listar los autores despliega lo que se observa en la Figura 3.

Figura 3. Despliegue del componente de listar.

Y el componente de despliegue de un detalle de un autor se detalla en la Figura 4.

Figura 4. Despliegue del detalle.

Editorial

El módulo EditorialModule tiene el componente EditorialListComponent. Dado que una editorial sólo tiene su nombre y la lista de libros que edita, hemos decidido no implementar un componente de detalle dado que el componente de listar las editoriales ya muestra para cada uno de sus libros (ver Figura 5).

Figura 5. Libros por editoriales

Las rutas de una aplicación se pueden definir en un único módulo de rutas que importa el módulo principal, o se pueden definir en los módulos funcionales. En cada módulo funcional se definen las rutas de ese módulo.

Esta última forma es la que vamos a desarrollar en este tutorial. Cada módulo define sus propias rutas.

La Figura 6 muestra el módulo principal AppModule y los tres módulos funcionales (BookModule, AuthorModule, EditorialModule). El módulo principal importa los demás módulos.

Figura 6. Módulos de la aplicación.

Hemos definido que cada módulo tenga su definición de rutas. Las rutas se definen en módulos que importan el RouterModule de Angular y que declaran un arreglo de rutas que se agrega a las rutas de la aplicación. La figura muestra la estrategia: cada módulo funcional importa su propio módulo de rutas (ver Figura 7).

Figura 7. Módulos de rutas en la aplicación.

Hemos definido que la página principal, es decir a la que llega el usuario al iniciar la aplicación sea la galería de libros. Entonces la ruta por defecto tiene asociado el componente de listar libros BookListarComponent. Esta ruta la definimos en el módulo AppRouting que es el módulo que importa el principal AppModule. Para esto edite el archivo app-routing.module.ts. Note que el arreglo de Routes se adiciona al módulo RouterModule a través de la función forRoot.

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { BookListComponent } from './book/book-list/book-list.component';


const routes: Routes = [
  { path: '', component: BookListComponent }
];


@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule {
}

Las rutas del módulo BookModule se definirán en su propio módulo denominado BookRoutingModule. Las rutas se van a asociar a una ruta principal books y en ejemplo se ven las dos rutas hijas: list, para listar los libros e :id para mostrar el detalle del libro identificado con el id. Por ejemplo, si el usuario selecciona un libro cuyo id es 100, la ruta es books/100. Cada una de las rutas tiene asociado el componente respectivo: BookListarComponent para books/list y BookDetailComponent para books/:id.

El arreglo de las rutas (Routes) es agregado al módulo de rutas utilizando la función forChild.

Este es el código del archivo que hay que crear en el módulo book con nombre book-routing.module.ts

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { BookListComponent } from './book-list/book-list.component';
import { BookDetailComponent } from './book-detail/book-detail.component';

const routes: Routes = [
    {
     path: 'list',
     component: BookListComponent
   },
   {
     path: ':id',
     component: BookDetailComponent
   },
];


@NgModule({
 imports: [RouterModule.forChild(routes)],
 exports: [RouterModule]
})
export class BookRoutingModule { }

Ahora, hay que actualizar el archivo de rutas de la aplicación app-routing.module.ts como se ve a continuación:

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { BookListComponent } from './book/book-list/book-list.component';


const routes: Routes = [
  { path: '', component: BookListComponent },
  { path: 'books', loadChildren: () => import('./book/book.module').then(m => m.BookModule) },
];


@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule {
}

La sintaxis de loadChildren puede parecer extraña pero lo que hace esta es importar el módulo de Book para obtener sus rutas y cargarlas como hijas de book. Esto hace parte de una función de Angular llamada Lazy Loading.

Luego se debe importar el nuevo módulo de rutas de book en el módulo principal así:

...
  imports: [
    BrowserModule,
    AppRoutingModule,
    BookModule,
    EditorialModule,
    HttpClientModule,
    AuthorModule,
    BookRoutingModule
  ],

...

Cada uno de los otros módulos sigue el mismo principio. La Figura 8 muestra el esquema global de las rutas.

La Figura 8. Esquema global de las rutas.

Para poder usar la directiva routerLink en la vista de los componentes se debe importar en los módulos que declaran los componentes el módulo RouterModule. Este es un ejemplo de cómo quedaría el módulo book.module.ts con esa nueva importación:

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';
import { RouterModule } from '@angular/router';
import { BookRoutingModule } from './book-routing.module';

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

Asegúrese de hacer esta actualización en los módulos de autor y editorial

Para asociar las rutas con los elementos de interfaz se usa un routerLink. Una forma de pensar en un routerLink es compararlo con el atributo href, solo que las rutas son las definidas en la aplicación y siguen el esquema de la figura anterior.

Cuando el usuario hace clic sobre el elemento que tiene definido el routerLink, el componente de la ruta se desplegará en donde hayamos definido la etiqueta router-outlet

La Figura 9 muestra el elemento de menú de Books. En la parte superior está el fragmento de código de definición de la ruta y luego la asociación del routerLink con la palabra Books y el router-outlet.

Figura 9. Menú para Books.

En nuestro ejemplo, solo hay un router-outlet y será definido en la vista del componente principal de la aplicación AppComponent. Cambie el siguiente código:

<app-book-list></app-book-list>

Por el siguiente:

<div class="container-fluid" id="principal">
    <div class="row">
      <div class="col-md-12">
        <div>
          <router-outlet></router-outlet>
        </div>
      </div>
    </div>
</div>
<footer class="page-footer">
    <div class="footer-copyright text-center py-2">
      Sample proyect -
      <a routerLink="/books/list" id="colorFooterText2"> Bookly</a>
    </div>
</footer>

La barra de navegación principal de la aplicación, con los elementos Book, Author y Editorial está en el siguiente fragmento de código.

...
<!-- Navigation bar -->
<!-- Navigation bar -->

<nav
 class="navbar navbar-expand-lg navbar-dark"
 style="background-color: #dc3545"
>
 <div class="container-fluid">
   <a class="navbar-brand" href="#">Bookly</a>
   <button
     class="navbar-toggler"
     type="button"
     data-bs-toggle="collapse"
     data-bs-target="#navbarNav"
     aria-controls="navbarNav"
     aria-expanded="false"
     aria-label="Toggle navigation"
   >
     <span class="navbar-toggler-icon"></span>
   </button>
   <div class="collapse navbar-collapse" id="navbarNav">
     <ul class="navbar-nav">
       <li class="nav-item">
         <a
           class="nav-link active"
           aria-current="page"
           routerLink="/books/list"
           >Books</a
         >
       </li>
       <li class="nav-item">
         <a class="nav-link active" routerLink="/authors/list">Authors</a>
       </li>
       <li class="nav-item">
         <a class="nav-link active" routerLink="/editorials/list">Editorials</a>
       </li>
     </ul>
   </div>
 </div>
</nav>
...

Nótese en el ejemplo anterior el uso del atributo routerLink dentro de las etiquetas a.

Ahora vamos a explicar cómo se navega desde la vista de un componente a otro.

Esta navegación debe ocurrir cuando desde el detalle de un libro se hace click en el nombre de un autor.

Paso 1: Definir las rutas del autor.

Este es el código del archivo que hay que crear en el módulo author con el nombre author-routing.module.ts

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { AuthorListComponent } from './author-list/author-list.component';
import { AuthorDetailComponent } from './author-detail/author-detail.component';


const routes: Routes = [
    {
     path: 'list',
     component: AuthorListComponent
   },
   {
     path: ':id',
     component: AuthorDetailComponent
   },
];




@NgModule({
 imports: [RouterModule.forChild(routes)],
 exports: [RouterModule]
})
export class AuthorRoutingModule { }

Ahora, hay que actualizar el archivo de rutas de la aplicación app-routing.module.ts como se ve a continuación:

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { BookListComponent } from './book/book-list/book-list.component';


const routes: Routes = [
  { path: '', component: BookListComponent },
  { path: 'books', loadChildren: () => import('./book/book.module').then(m => m.BookModule) },
  { path: 'authors', loadChildren: () => import('./author/author.module').then(m => m.AuthorModule) }
];


@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule {
}

Luego se debe importar el nuevo módulo de rutas de author en el módulo principal así:

...
  imports: [
    BrowserModule,
    AppRoutingModule,
    BookModule,
    EditorialModule,
    HttpClientModule,
    AuthorModule,
    BookRoutingModule,
    AuthorRoutingModule
  ],

...

Por último importe RouterModule y AuthorRoutingModule al módulo de Author:

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { AuthorListComponent } from './author-list/author-list.component';
import { AuthorDetailComponent } from './author-detail/author-detail.component';
import { RouterModule } from '@angular/router';
import { AuthorRoutingModule } from './author-routing.module';


@NgModule({
  imports: [
    CommonModule,
    RouterModule,
    AuthorRoutingModule
  ],
  declarations: [AuthorListComponent, AuthorDetailComponent]
})
export class AuthorModule { }

Paso 2: Definir el routerLink en el detalle del libro.

La asociación de rutas a elementos como textos, imágenes, etc, sigue el mismo principio explicado antes: se incluye el elemento routerLink con la ruta especificada. Para el caso de los autores esperamos una ruta de la forma /authors/1 (suponiendo que hicimos clic en el autor con el id 1). Como en la lista de autores se itera sobre el arreglo authors, podemos tener acceso al id de un autor con el atributo author.id. De esta forma, el valor del atributo routerLink quedaría así:

routerLink="/authors/{{ author.id }}"

El código del componente

BookDetailComponent quedaría así:

<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 class="caption" *ngFor="let author of bookDetail.authors">
         <a routerLink="/authors/{{ author.id }}">{{ author.name }}</a>
       </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>

Paso 3: Cargar la información del autor

El componente AuthorDetailComponent tiene un parámetro de entrada @Input() que corresponde al autor que se va a desplegar. Sin embargo, en este llamado utilizando el routerLink no enviamos el objeto AuthorDetail sino que enviamos en la ruta el id de dicho autor.

Entonces debemos tomar el id del autor que llega por la ruta y enviar ese id a un método del servicio que retorne la información de ese autor, Para esto, debemos:

  1. Agregar un método al servicio para que dado el id del autor lo traiga utilizando http.get. Este método retorna un Observable
  2. Agregar un método en el componente de detalle para que se suscriba al Observable que retorna el servicio.

Este es el código para el servicio AuthorService

import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { AuthorDetail } from './author-detail';
import { environment } from './../../environments/environment.development';


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


 private apiUrl: string = environment.baseUrl + 'authors';


 constructor(private http: HttpClient) { }


 getAuthors(): Observable<AuthorDetail[]> {
   return this.http.get<AuthorDetail[]>(this.apiUrl);
 }


 getAuthor(id: string): Observable<AuthorDetail> {
   return this.http.get<AuthorDetail>(this.apiUrl + "/" + id);
 }


}

En el componente, además de la suscripción al Observable, tenemos que identificar si el llamado fue por el selector o por la ruta. Si el Input es undefined, significa que el llamado fue utilizando la ruta. Si es por la ruta entonces tenemos que obtener el id de la ruta. Para esto, utilizamos la librería ActivatedRoute que ofrece servicios para obtener los valores de la ruta.

import { Component, Input, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { AuthorDetail } from '../author-detail';
import { AuthorService } from '../author.service';

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

 authorId!: string;
 @Input() authorDetail!: AuthorDetail;

 constructor(
   private route: ActivatedRoute,
   private authorService: AuthorService
 ) {}

 getAuthor(){
   this.authorService.getAuthor(this.authorId).subscribe(author=>{
     this.authorDetail = author;
   })
 }

 ngOnInit() {
   if(this.authorDetail === undefined){
     this.authorId = this.route.snapshot.paramMap.get('id')!
     if (this.authorId) {
       this.getAuthor();
     }
   }
 }
}

Con estos ajustes, cuando se hace clic en el nombre del autor la aplicación navega a la ruta /authors/id_autor, el componente toma el id, se conecta al servicio y muestra el detalle de ese autor.

Esta navegación debe ocurrir cuando desde el detalle de un autor se hace click en el nombre de un libro.

Paso 1: Definir el routerLink en el detalle del autor.

La asociación de rutas a elementos como textos, imágenes, etc, sigue el mismo principio explicado antes: se incluye el elemento routerLink con la ruta especificada. Para el caso de los libros esperamos una ruta de la forma /books/1 (suponiendo que hicimos clic en el book con el id 1). Como en la lista de libros se itera sobre el arreglo books, podemos tener acceso al id de un book con el atributo book.id. De esta forma, el valor del atributo routerLink quedaría así:

routerLink="/books/{{ book.id }}"

El código del componente AuthorDetailComponent quedaría así:

<div class="panel-body">
  <div class="border margin p-3">
    <p class="h3 p-2 author-name">{{ authorDetail.name }}</p>
    <p></p>
    <div class="row">
      <div class="col-md-3">
        <div class="thumb">
          <img
            class="img-fluid img-thumbnail border border-danger"
            src="{{ authorDetail.image }}"
            alt="{{ authorDetail.name }}"
          />
        </div>
      </div>
      <div class="col-md-7">
        <dl>
          <dt class="text-title">Bio</dt>
          <dd class="text">{{ authorDetail.description }}</dd>
          <dt class="text-title">BirthDay</dt>
          <dd class="text">{{ authorDetail.birthDate }}</dd>
        </dl>
        <p class="h3">Books by this author:</p>
        <ul>
          <li
            *ngFor="let book of authorDetail.books"
          >
            <a routerLink="/books/{{ book.id }}">{{ book.name }}</a>
          </li>
        </ul>
      </div>
    </div>
  </div>
 </div>

Paso 2: Cargar la información del libro

El componente BookDetailComponent tiene un parámetro de entrada @Input() que corresponde al libro que se va a desplegar. Sin embargo, en este llamado utilizando el routerLink no enviamos el objeto BookDetail sino que enviamos en la ruta el id de dicho libro.

Entonces debemos tomar el id del libro que llega por la ruta y enviar ese id a un método del servicio que retorne la información de ese libro, Para esto, debemos:

  1. Agregar un método al servicio para que dado el id del libro lo traiga utilizando http.get. Este método retorna un Observable
  2. Agregar un método en el componente de detalle para que se suscriba al Observable que retorna el servicio.

Este es el código para el servicio BookService

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';


import { environment } from './../../environments/environment.development';
import { Observable } from 'rxjs';
import { BookDetail } from './bookDetail';


@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);
 }


 getBook(id: string): Observable<BookDetail> {
   return this.http.get<BookDetail>(this.apiUrl + "/" + id);
 }


}

En el componente, además de la suscripción al Observable, tenemos que identificar si el llamado fue por el selector o por la ruta. Si el Input es undefined, significa que el llamado fue utilizando la ruta. Si es por la ruta entonces tenemos que obtener el id de la ruta. Para esto, utilizamos la librería ActivatedRoute que ofrece servicios para obtener los valores de la ruta.

Este es el contenido del archivo book-detail.component.ts

import { Component, Input, OnInit } from '@angular/core';
import { BookDetail } from '../bookDetail';
import { ActivatedRoute } from '@angular/router';
import { BookService } from '../book.service';


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

    bookId!: string;
    @Input() bookDetail!: BookDetail;

    constructor(

        private route: ActivatedRoute,
        private bookService: BookService
     
     ) { }

    getBook(){
        this.bookService.getBook(this.bookId).subscribe(book=>{
          this.bookDetail = book;
        })
      }
     

    ngOnInit() {
        if(this.bookDetail === undefined){
            this.bookId = this.route.snapshot.paramMap.get('id')!
            if (this.bookId) {
              this.getBook();
            }
        }
    }

}

Con estos ajustes, cuando se hace clic en el nombre del libro la aplicación navega a la ruta /books/bookId, el componente toma el id, se conecta al servicio y muestra el detalle de ese libro.

Esta navegación debe ocurrir cuando desde la lista de editoriales se hace click en la carátula de un libro.

Paso 1: Definir las rutas de la editorial.

Este es el código del archivo que hay que crear en el módulo editorial con el nombre editorial-routing.module.ts

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { EditorialListComponent } from './editorial-list/editorial-list.component';

const routes: Routes = [
    {
     path: 'list',
     component: EditorialListComponent
   },
];

@NgModule({
 imports: [RouterModule.forChild(routes)],
 exports: [RouterModule]
})
export class EditorialRoutingModule { }


Ahora, hay que actualizar el archivo de rutas de la aplicación app-routing.module.ts como se ve a continuación:

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { BookListComponent } from './book/book-list/book-list.component';


const routes: Routes = [
  { path: '', component: BookListComponent },
  { path: 'books', loadChildren: () => import('./book/book.module').then(m => m.BookModule) },
  { path: 'authors', loadChildren: () => import('./author/author.module').then(m => m.AuthorModule) },
  { path: 'editorials', loadChildren: () => import('./editorial/editorial.module').then(m => m.EditorialModule) }
];


@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule {
}

Luego se debe importar el nuevo módulo de rutas de editorial en el módulo principal así:

...
  imports: [
    BrowserModule,
    AppRoutingModule,
    BookModule,
    EditorialModule,
    HttpClientModule,
    AuthorModule,
    BookRoutingModule,
    AuthorRoutingModule,
    EditorialRoutingModule
  ],
...

Por último importe RouterModule y EditorialRoutingModule al módulo de editorial:

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { EditorialListComponent } from './editorial-list/editorial-list.component';
import { RouterModule } from '@angular/router';
import { EditorialRoutingModule } from './editorial-routing.module';

@NgModule({
  imports: [
    CommonModule,
    RouterModule,
    EditorialRoutingModule
  ],
  declarations: [EditorialListComponent]
})
export class EditorialModule { }

Paso 2: Definir el routerLink en la imagen del libro.

Para el caso de los libros esperamos una ruta de la forma /books/1 (suponiendo que hicimos clic en el book con el id 1). Como en la lista de libros se itera sobre el arreglo books, podemos tener acceso al id de un book con el atributo book.id. De esta forma, el valor del atributo routerLink quedaría así:

routerLink="/books/{{ book.id }}"

El código del componente EditorialListComponent quedaría así:

<div class="container-fluid mt-2">
 <h2 class="text-center">Editorials</h2>
 <div class="row" *ngFor="let editorial of editorials">
   <div class="col-2">
     <p>{{ editorial.name }}</p>
   </div>
   <div class="col-10">
     <div class="row">
       <div
         class="card p-2 mb-2"
         style="width: 7rem; height: 10rem"
         *ngFor="let book of editorial.books"
       >
         <img
           class="card-img-top"
           src="{{ book.image }}"
           alt="{{ book.name }}"
           routerLink="/books/{{ book.id }}"
         />
       </div>
     </div>
   </div>
   <hr />
 </div>
</div>

Paso 3: Cargar la información del libro

El componente BookDetailComponent ya está organizado para recibir el libro que debe mostrar, bien sea como parámetro del componente o como parámetro en de la ruta.

En este tutorial y comparado con el de maestro/detalle hemos agregado varios componentes adicionales:

También hemos creado dos servicios adicionales:

Al ejecutar las pruebas por defecto aparecerán varios errores. Recuerde que para arreglar esos errores (siguiendo lo mencionado en los tutoriales de listar y detalle) se debe:

Importar el módulo HttpClientTestingModule en la prueba de los servicios

Importar el módulo HttpClientModule en la prueba de los componentes

Al agregar estos módulos aún seguirá apareciendo el siguiente error:

NullInjectorError: R3InjectorError(DynamicTestModule)[ActivatedRoute -> ActivatedRoute]: 
  NullInjectorError: No provider for ActivatedRoute!

Esto ocurre porque al utilizar routerLink se agregó el módulo RouterModule a cada módulo funcional; por tanto, se requiere importar en las pruebas de cada componente el módulo RouterTestingModule.

Este es entonces el fragmento de código para la prueba del componente BookListComponent. Ahí se observa en el arreglo imports la referencia a RouterTestingModule.

 beforeEach(async(() => {
    TestBed.configureTestingModule({
      imports: [HttpClientModule, RouterTestingModule],
      declarations: [ BookListComponent ],
      providers: [ BookService ]
    })
    .compileComponents();
  }));

Incluya esa importación en todas las pruebas de los componentes de detalle y ejecute de nuevo las pruebas que deberán funcionar correctamente.