logocloudstic

Resumen

Angular se ha convertido en uno de los frameworks más populares. Entre sus grandes ventajas destacan el desarollo basado componentes y su polivalencia para desarrollo web y móvil.

En este tutorial se aborda el desarrollo por etapas de una aplicación maestro detalle basada en servicios. El objetivo real es aprender interactuar con servicios REST y navegar entre páginas con llamadas a servicios diferentes y aprender a tratar con la asincronía de las peticiones. No obstante, hay situaciones, como en el desarrollo de un prototipo inicial, en las que puede ser necesario desarrollar servicios con datos mockeados en memoria. Por ello, dedicaremos un espacio a este tipo de servicios que trabajan con datos en memoria, los cuales están exentos de las cuestiones de la asincronía.

Objetivos
  • Crear un proyecto sencillo en Angular

  • Entender el uso de rutas en Angular

  • Usar el componente Router para la carga dinámica de páginas

  • Usar el patrón de inyección de dependencias para el uso de servicios y captura de parámetros

  • Crear servicios de generación de datos síncronos

  • Crear servicios asíncronos basados en APIs REST

  • Consumir de servicios de forma asíncrona

  • Presentar datos de los componentes en las vistas

Disponible el repositorio usado en este tutorial.

1. Creación de los elementos básicos del proyecto

El proyecto que vamos a crear está formado iniciamente por dos páginas (home y about) e incluirá una barra de navegación en la parte superior. Usaremos también Bootstrap, lo que le dará un aspecto similar al de la figura:

inicial

Desde una terminal comenzamos creando el proyecto con el CLI de Angular

ng new miapp (1)
cd miapp
1 Nos pedirá si queremos añadir enrutado Angular y el tipo de formato de hojas de esilo que queremos usar. Dejamos las opciones por defecto (No usar enrutado Angular y CSS como formato de hojas de estilo)
Opciones en la creación del proyecto
ng new miapp
? Would you like to add Angular routing? No
? Which stylesheet format would you like to use? (Use arrow keys)
❯ CSS
  SCSS   [ https://sass-lang.com/documentation/syntax#scss                ]
  Sass   [ https://sass-lang.com/documentation/syntax#the-indented-syntax ]
  Less   [ http://lesscss.org                                             ]
  Stylus [ http://stylus-lang.com                                         ]

1.1. Instalación de Bootstrap

Para el uso de Bootstrap podemos optar por incluir el CSS en la cabecera y los JS al final del archivo index.html. Esta es una de las formas más populares de usar Bootstrap, aunque no es la forma que seguiremos en este tutorial. A continuación se muestra un fragmento de cómo quedaría el index.html si siguiéramos esta opción.

...
<head>
...
  <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css" integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk" crossorigin="anonymous"> (1)
...
</head>
<body>
...
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script> (2)
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.min.js" integrity="sha384-OgVRvuATP1z7JjHLkuOU7Xw704+h835Lr+6QL9UvYjZE3Ipu6Tp75j7Bh/kR0JKI" crossorigin="anonymous"></script>
</body>
...
1 Inclusión de la hoja de estilos en el <head>
2 Inclusión de los JS al final del <body>

La opción que elegiremos para incluir Bootstrap en nuestro proyecto será usarlo como un paquete más del proyecto siguiendo estos pasos. En primer lugar, instalamos los paquetes necesarios (bootstrap, jquery y @popperjs/core) y usaremos la opción --save para guardar sus dependencias en el archivo package.json. Esto hará que estos paquetes se instalen cuando otros utilicen el proyecto e instalen las dependencias con npm install.

npm install bootstrap jquery @popperjs/core --save

Incluir en la sección architect.build de angular.json las referencias al CSS y los JS. Esto es equivalente a incluir en el index.html el CSS y los JS de Boostrap.

...
            "styles": [
              "src/styles.css",
              "node_modules/bootstrap/dist/css/bootstrap.min.css"
            ],
            "scripts": [
              "node_modules/jquery/dist/jquery.min.js",
              "node_modules/@popperjs/core/dist/umd/popper.min.js",
              "node_modules/bootstrap/dist/js/bootstrap.min.js"
            ]
...

1.2. Creación de los componentes

Organizaremos los componentes y los servicios de la aplicación en carpetas:

  • Una carpeta components para los componentes.

  • Una carpeta componentes/shared para componentes compartidos (p.e. la barra de navegación)

  • Una carpeta services para servicios que veremos más adelante cuando creemos el primer servicio.

Creamos los componentes con el CLI de Angular

ng generate component components/home
ng generate component components/about
ng generate component components/shared/navbar
Abreviaturas en el CLI de Angular

El CLI de Angular permite abreviaturas (g para generate, c para component, …​).

Los componentes anteriores también se podrían haber creado usando la forma abreviada de esta forma:

ng g c components/home
ng g c components/about
ng g c components/shared/navbar

Los componentes generados son añadidos de forma automática al array declarations en app.module.ts.

...
@NgModule({
  declarations: [
    AppComponent,
    HomeComponent, (1)
    AboutComponent,
    NavbarComponent
  ],
...
1 Componentes incoporados
Archivos generados al crear un componente

De forma predeterminada, al crear un componente se generan 4 archivos:

  • Un CSS para estilos específicos a aplicar únicamente en el componente (.css).

  • Un HTML para la vista del componente (.html).

  • Un archivo TypeScript para programar la lógica del componente (.ts).

  • Un archivo para las pruebas (.spec.ts).

Las aplicaciones Angular se desarrollan como un conjunto de componentes que pueden anidarse. En el archivo TypeScript se especifica qué etiqueta se debe utilizar para usar el componente en HTML. Esta etiqueta es lo que se conoce como selector.

A continuación se muestra el código TypeScript de la barra de navegación en el que se puede apreciar el selector generado por el CLI de Angular.

import { Component, OnInit } from '@angular/core';

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

  constructor() { }

  ngOnInit(): void {
  }

}
1 Selector a utilizar para usar el componente de la barra de navegación en otro componente.

1.3. Implementación de la página home

Sustituimos el contenido de components/home.component.html por un jumbotron de Bootstrap.

<div class="jumbotron">
  <h1 class="display-4">Hello, world!</h1>
  <p class="lead">
    This is a simple hero unit, a simple jumbotron-style component for calling
    extra attention to featured content or information.
  </p>
  <hr class="my-4" />
  <p>
    It uses utility classes for typography and spacing to space content out
    within the larger container.
  </p>
  <a class="btn btn-primary btn-lg" href="#" role="button">Learn more</a>
</div>

1.4. Implementación de la barra de navegación

Sustituimos el contenido de components/shared/navbar.html por un navbar sin cuadro de búsqueda de Bootstrap. En las opciones de menú dejamos sólo dos elementos (uno para home y otro para about)

<nav class="navbar navbar-expand-lg navbar-light bg-light">
  <a class="navbar-brand" href="#">Navbar</a>
  <button
    class="navbar-toggler"
    type="button"
    data-toggle="collapse"
    data-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 active">
        <a class="nav-link" href="#">Home</a> (1)
      </li>
      <li class="nav-item">
        <a class="nav-link" href="#">About</a> (2)
      </li>
    </ul>
  </div>
</nav>
1 Elemento Home al que se ha eliminado el <span> que trae por defecto el código del navbar
2 Elemento About

1.5. Enrutado

Las rutas permitidas en nuestra aplicación estarán definidas en el archivo app.routes.ts. Inicialmente, definiremos una ruta para el home y otra para el about. Más adelante, conforme vayamos incorporando funcionalidades a la aplicación añadiremos más rutas a este archivo.

import { Routes } from '@angular/router';
import { HomeComponent } from './components/home/home.component';
import { AboutComponent } from './components/about/about.component';

export const ROUTES: Routes = [
  { path: 'home', component: HomeComponent }, (1)
  { path: 'about', component: AboutComponent },
  { path: '', pathMatch: 'full', redirectTo: 'home' }, (2)
  { path: '**', pathMatch: 'full', redirectTo: 'home' }, (3)
];
1 Para cada ruta se define un par formado por el path a añadir a la URL de la aplicación junto a su componente asociado.
2 Redirigir a home la ruta vacía
3 Redirigir a home cualquier otra ruta no válida

1.5.1. Importación del archivo de rutas

Importar en app.module.ts las rutas definidas.

...
  imports: [BrowserModule, RouterModule.forRoot(ROUTES, { useHash: true })],
...

1.6. Modificar la página de la aplicación

La página de la aplicación se construye mediante la inclusión de componentes existentes. En el app.component.html dejamos sólo los selectores de los componentes que incluiremos en la página (la barra de navegación y el componente seleccionado desde la barra de navegación).

<app-navbar></app-navbar> (1)
<div class="container m-5">
  <router-outlet></router-outlet> (2)
</div>
1 Selector del componente de la barra de navegación
2 Marcador que incluye de forma dinámica el componente de la ruta seleccionada

1.7. Incluir las rutas en la barra de navegación

Para cada elemento de la barra de navegación incluiremos su ruta correspondiente definida previamente en app.routes.ts

<nav class="navbar navbar-expand-lg navbar-light bg-light">
  <a class="navbar-brand" href="#">Navbar</a>
  <button
    class="navbar-toggler"
    type="button"
    data-toggle="collapse"
    data-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" routerLinkActive="active"> (1)
        <a class="nav-link" routerLink="home">Home</a> (2)
      </li>
      <li class="nav-item" routerLinkActive="active">
        <a class="nav-link" routerLink="about">About</a> (3)
      </li>
    </ul>
  </div>
</nav>
1 Dejar la clase sólo a nav-item. El estilo active quedará determinado en función de la ruta seleccionada
2 routerLink toma el valor del path definido en app.routes.ts
3 Path de la ruta about en app.routes.ts

1.8. Servir la aplicación

Desde el directorio de la aplicación con ng serve --open se ejecuta la aplicación y se abre en un navegador al guardar los cambios.

La aplicación queda en modo de live reload. Cualquier cambio que se realice sobre el código se verá de forma inmediata en el navegador.

inicial

2. Creación de un servicio con datos en memoria

Al comenzar a desarrollar una aplicación basada en servicios, como puede ser una aplicación que interactúe con una base de datos a través de una API REST, es frecuente no contar con la API a la hora de desarrollar el frontend. En estas situaciones podemos simular el funcionamiento del servicio generando unos datos de prueba (p.e. en JSON) y desarrollar el frontend a partir de esos datos de prueba. Una vez desarrollada la API REST se podrán cambiar los servicios para que interactúen con la base de datos en lugar de los datos de prueba.

2.1. Creación del servicio

Los componentes de Angular no deben interactuar directamente con los datos. Los componentes deben dedicarse a presentar los datos y delegando en los servicios el acceso a los datos. Posteriormente, los servicios se inyectarán en los componentes que quieran usarlos (para más información consultar la inyeccion de dependencias en Angular).

ng generate service services/employees (1)
1 Guardaremos los servicios en una carpeta services

Añadir el servicio creado al array providers en app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppComponent } from './app.component';
import { HomeComponent } from './components/home/home.component';
import { AboutComponent } from './components/about/about.component';
import { NavbarComponent } from './components/shared/navbar/navbar.component';
import { RouterModule } from '@angular/router';
import { ROUTES } from './app.routes';
import { EmployeesService } from './services/employees.service'; (1)

@NgModule({
  declarations: [AppComponent, HomeComponent, AboutComponent, NavbarComponent],
  imports: [BrowserModule, RouterModule.forRoot(ROUTES, { useHash: true })],
  providers: [EmployeesService], (2)
  bootstrap: [AppComponent],
})
export class AppModule {}
1 Archivo del servicio
2 Servicio añadido al array providers

A diferencia de los componentes, que sí son añadidos de forma automáticamente al array providers de app.module.ts, los servicios no son añadidos de forma automáticamente. Por tanto, tras la creación de un servicio hay que añadirlo de forma manual al array providers en app.module.ts.

2.2. Implementación del servicio

Para crear el mock y evitar la interacción con una base de datos crearemos un array donde guardaremos los datos en JSON, tal y como los devolvería la API que está pendiente de crear.

import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root',
})
export class EmployeesService {
  employees: any[] = [ (1)
    {
      id: 0,
      isActive: false,
      age: 39,
      name: {
        first: 'Patsy',
        last: 'Moore',
      },
      company: 'ZYTREX',
      email: 'patsy.moore@zytrex.net',
      favoriteFruit: 'strawberry',
    },
    {
      id: 1,
      isActive: true,
      age: 37,
      name: {
        first: 'Valencia',
        last: 'Flores',
      },
      company: 'AQUAMATE',
      email: 'valencia.flores@aquamate.us',
      favoriteFruit: 'banana',
    },
    {
      id: 2,
      isActive: false,
      age: 37,
      name: {
        first: 'Leona',
        last: 'Wyatt',
      },
      company: 'SENMAO',
      email: 'leona.wyatt@senmao.io',
      favoriteFruit: 'apple',
    },
  ];
  constructor() {}

  getEmployees() { (2)
    return this.employees;
  }

  getEmployee(id: number) { (3)
    return this.employees[id];
  }
}
1 Array con los datos de prueba
2 Método que devuelve los datos de todos los empleados para la parte maestra
3 Método que devuelve los datos de un empleado para la parte de detalle

2.3. Creación del componente de la parte maestra

Crearemos un componente encargado de gestionar los datos de los empleados y encargarse de su presentación. Se trata de la parte maestra. Más adelante, se podrá seleccionar un empleado de la lista para acceder a sus detalles.

ng g c components/employees

2.4. Actualización del archivo de rutas

Añadir una nueva ruta employees a app.routes.ts asociada al componente de los empleados.

...
  { path: 'employees', component: EmployeesComponent },
...

2.5. Actualización de la barra de menú

La lista de empleados será accesible desde el menú. Por tanto, hay que añadir una entrada en la barra de menús que conecte con la ruta employees definida en el paso anterior.

...
    <ul class="navbar-nav">
      <li class="nav-item" routerLinkActive="active">
        <a class="nav-link" routerLink="home">Home</a>
      </li>
      <li class="nav-item" routerLinkActive="active"> (1)
        <a class="nav-link" routerLink="employees">Employees</a> (2)
      </li>
      <li class="nav-item" routerLinkActive="active">
        <a class="nav-link" routerLink="about">About</a>
      </li>
    </ul>
...
1 Nueva entrada en la lista de items de la barra de navegación
2 Conexión del elemento de menú con la ruta definida en app.routes.ts

Con los cambios realizados la aplicación deberá estar como la de la figura. El router se encarga de sustituir dinámicamente la presentación del componente seleccionado en la ruta. De eso se encarga el selector router-outlet que incluimos en app.component.html.

employeeMenu

2.6. Implementación del controlador de la parte maestra

Los servicios son los encargados de tratar con los datos.

Pasos a seguir en la implementación del controlador
  • El servicio se inyecta en el constructor para tener acceso en la clase a los métodos implementados en el servicio.

  • Crear variables de instancia en el controlador para almacenar los datos con los que tratan los servicios.

  • En la implementación del constructor se llamará al servicio para que obtenga los datos y los cargue en las variables de instancia del controlador definidas para ello.

  • La vista accederá a las variables de instancia del controlador para presentar los datos.

import { Component, OnInit } from '@angular/core';
import { EmployeesService } from '../../services/employees.service';

@Component({
  selector: 'app-employees',
  templateUrl: './employees.component.html',
  styleUrls: ['./employees.component.css'],
})
export class EmployeesComponent implements OnInit {
  employees: any[] = []; (1)

  constructor(private employeesService: EmployeesService) { (2)
    this.employees = employeesService.getEmployees(); (3)
  }

  ngOnInit(): void {}
}
1 Variable de instancia para almacenar los datos recuperados por el servicio
2 Inyección del servicio para que esté disponible en la clase
3 Almacenamiento en la variable de instancia de los datos recuperados por el servicio

2.7. Crear la vista de la parte maestra

<div class="row">
  <div class="col">
    <ul>
      <li *ngFor="let employee of employees"> (1)
        {{ employee.name.first }} {{ employee.name.last }} (2)
      </li>
    </ul>
  </div>
</div>
1 Iteración sobre la variable de instancia employees
2 Presentación de datos mediante interpolación

Consultar más información sobre la interpolación en la documentación de Angular.

El resultado será similar al de la figura.

listadoEmpleados

2.8. Crear el componente para el detalle

Una vez creada la parte maestra pasamos al detalle, que mostrará información asociada al elemento seleccionado en el maestro. Comenzamos creando el componente

ng generate component components/employee

2.9. Actualización del archivo de rutas

Añadir una nueva ruta employee/:id a app.routes.ts asociada al componente del detalle de un empleado.

...
  { path: 'employee/:id', component: EmployeeComponent},
...

En el caso de este detalle no añadiremos ningún acceso directo desde el menú. El detalle tiene definida una ruta pero no se llega al detalle desde el menú, sino seleccionando el empleado en el listado

2.10. Añadir al controlador maestro el acceso al detalle

El controlador maestro tiene que proporcionar un método que se encargue de ir al detalle. Ese método tomará como argumento el id del empleado a mostrar y creará una ruta válida (definida en app.routes.ts) a partir del id.

...
export class EmployeesComponent implements OnInit {
  employees: any[] = [];

  constructor(
    private employeesService: EmployeesService,
    private router: Router (1)
  ) {
    this.employees = employeesService.getEmployees();
  }

  showEmployee(id: number) { (2)
    this.router.navigate(['employee', id]); (3)
  }

  ngOnInit(): void {}
}
1 Inyección de un objeto router que permitirá la navegación a la página de detalle
2 Método que implementa el acceso al detalle
3 El método navigate toma como argumentos un array con la lista de literales y/o parámetros a añadir a la URL para confeccionar la ruta.

2.11. Añadir a la vista del maestro el acceso al detalle

Se trata de encerrar entre un hipervínculo el valor del empleado mostrado en el maestro.

  • routerLink le dará formato de hipervínculo.

  • Se creará un evento (click) para llamar al método implementado en el controlador.

<div class="row">
  <div class="col">
    <ul>
      <li *ngFor="let employee of employees">
        <a routerLink="" (click)="showEmployee(employee.id)"> (1)
          {{ employee.name.first }} {{ employee.name.last }}
        </a>
      </li>
    </ul>
  </div>
</div>
1 routerLink hace que tenga estilo de hipervínculo. De la navegación se encarga el método showEmployee(), que toma como argumento el id del empleado seleccinado.

El resultado será como el de la figura siguiente.

listadoEmpleadoConLinks

2.12. Implementar el controlador de la parte de detalle

El controlador del detalle tiene que tomar el valor del parámetro pasado en la ruta. Los parámetros se reciben en un array denominado params. Como en el archivo app.routes.ts se definió la ruta employee/:id el parámetro se recibe en params['id'].

import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { EmployeesService } from '../../services/employees.service';

@Component({
  selector: 'app-employee',
  templateUrl: './employee.component.html',
  styleUrls: ['./employee.component.css'],
})
export class EmployeeComponent implements OnInit {
  employee: any = {}; (1)
  constructor(
    private activatedRoute: ActivatedRoute, (2)
    private employeesService: EmployeesService (3)
  ) {
    this.activatedRoute.params.subscribe((params) => { (4)
      this.employee = this.employeesService.getEmployee(params['id']); (5)
    });
  }

  ngOnInit(): void {}
}
1 Variable de instancia para almacenar los datos recuperados por el servicio
2 Inyección de la ruta para poder acceder al parámetro proporcionado
3 Inyección del servicio para recuperar los detalles del empleado
4 Obtener los parámetros. Se usa subscribe porque se trata de un Observable ya que se trata de una recepción asíncrona.

2.13. Implementar la vista de la parte de detalle

La vista presenta los valores almacenados en la variable de instancia employee.

<div class="row">
  <div class="col">
    <h2>{{ employee.name.first }} {{ employee.name.last }}</h2> (1)
    <p>Company: {{ employee.company }}</p>
    <p>Contact at: {{ employee.email }}</p>
  </div>
</div>
1 Uso de la interpolación para la presentación de datos

El resultado de acceso a un detalle será similar a este.

DetalleEmpleado

3. Creación de un servicio sobre una API REST

Aquí veremos un ejemplo más real de la creación de un servicio. En nuestro caso, normalmente desarrollaremos servicios basados en APIs REST. Esto supone un modelo asíncrono en el que básicamente se realizan las peticiones y la respuesta no llega de forma inmediata. En su lugar, quedamos suscritos a la resolución de la petición.

Gran parte de los pasos son similares a los vistos en el apartado anterior de servicios basados en datos en memoria. Sin embargo, hay algunos ligeros cambios derivados de la naturaleza asíncrona de las peticiones HTTP. Aquí les dedicaremos especial atención.

JSONPlaceholder ofrece un servicio para prueba y prototipado rápido. Para este ejemplo de maestro detalle nos centraremos en dos endpoints:

3.1. Creación del servicio

ng generate service services/posts (1)
1 Guardaremos los servicios en una carpeta services

Añadir el servicio creado al array providers en app.module.ts

...
  providers: [EmployeesService, PostsService],
...

3.2. Implementación del servicio

Importación de HttpClientModule

El servicio usará la clase HttpClient. Para usar esta clase es necesario que previamente se haya importado HttpClientModule. La mayoría de las aplicaciones realizan esta importación en app.module.ts.

...
  imports: [
    BrowserModule,
    RouterModule.forRoot(ROUTES, { useHash: true }),
    HttpClientModule,(1)
  ],
...
1 Importar el provider HttpClientModule

No importar este módulo provocaría este error al usar el servicio indicando que no existe provider para HttpClient:

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

@Injectable({
  providedIn: 'root',
})
export class PostsService {
  constructor(private http: HttpClient) {} (1)

  getQuery(query: string) { (2)
    const url = `https://jsonplaceholder.typicode.com/${query}`;
    return this.http.get(url);
  }

  getPosts() { (3)
    return this.getQuery('posts');
  }

  getComments(id: string) { (4)
    return this.getQuery(`posts/${id}/comments`);
  }
}
1 Inyección de un objeto HTTP que permita lanzar peticiones get sobre la API REST
2 Método auxiliar donde se recoge la parte común de la URL de los endpoints de la API. Se le pasa como parámetro lo que varía de un endpoint a otro
3 Método que recupera todos los posts
4 Método que recupera todos los comentarios de un post

3.3. Creación del componente de la parte maestra

Crearemos un componente encargado de gestionar los datos de los posts y encargarse de su presentación. Se trata de la parte maestra. Más adelante, se podrá seleccionar un post de la lista para acceder a sus comentarios.

ng generate component components/posts

3.4. Actualización del archivo de rutas

Añadir una nueva ruta posts a app.routes.ts asociada al componente de los posts.

export const ROUTES: Routes = [
...
  { path: 'posts', component: PostsComponent },
...
];

3.5. Actualización de la barra de menú

...
      <li class="nav-item" routerLinkActive="active">
        <a class="nav-link" routerLink="employees">Employees</a>
      </li>
      <li class="nav-item" routerLinkActive="active"> (1)
        <a class="nav-link" routerLink="posts">Posts</a> (2)
      </li>
      <li class="nav-item" routerLinkActive="active">
        <a class="nav-link" routerLink="about">About</a>
      </li>
...
1 Nueva entrada en la lista de items de la barra de navegación
2 Conexión del elemento de menú con la ruta definida en app.routes.ts

Con los cambios realizados la aplicación deberá estar como la de la figura. El router se encarga de sustituir dinámicamente la presentación del componente seleccionado en la ruta. De eso se encarga el selector router-outlet que incluimos en app.component.html.

postsMenu

3.6. Implementación del controlador de la parte maestra

Seguiremos estos pasos para implementar el controlador:

  1. Inyectar el servicio en el constructor para tener acceso en la clase a los métodos implementados en el servicio.

  2. Crear variables de instancia en el controlador para almacenar los datos con los que tratan los servicios.

  3. Llamar al servicio en la implementación del constructor para que obtenga los datos y los cargue en las variables de instancia definidas para ello.

La vista accederá a las variables de instancia del controlador para presentar los datos.

import { Component, OnInit } from '@angular/core';
import { PostsService } from '../../services/posts.service';

@Component({
  selector: 'app-posts',
  templateUrl: './posts.component.html',
  styleUrls: ['./posts.component.css'],
})
export class PostsComponent implements OnInit {
  posts: any[] = []; (1)

  constructor(private postsService: PostsService) { (2)
    this.postsService.getPosts().subscribe((data: any) => { (3)
      this.posts = data; (4)
    });
  }

  ngOnInit(): void {}
}
1 Variable de instancia para almacenar los datos recuperados por el servicio
2 Inyección del servicio para que esté disponible en la clase
3 Suscripción al observable que devuelve los posts. La respuesta asíncrona se resuelve mediante una función de flecha que almacena en data los datos recuperados por el servicio
4 Almacenamiento en la variable de instancia de los datos recuperados por el servicio

3.7. Crear la vista de la parte maestra

<div class="row">
  <div class="col">
    <ul>
      <li *ngFor="let post of posts"> (1)
        {{ post.title }} (2)
      </li>
    </ul>
  </div>
</div>
1 Iteración sobre la variable de instancia posts
2 Presentación de datos mediante interpolación

El resultado será similar al de la figura.

listadoPosts

3.8. Crear el componente para el detalle

ng generate component components/comments

3.9. Actualización del archivo de rutas

Añadir una nueva ruta posts/:id/comments a app.routes.ts asociada al componente del detalle de un post.

...
  { path: 'posts/:id/comments', component: CommentsComponent },
...

En el caso de este detalle no añadiremos ningún acceso directo desde el menú. El detalle tiene definida una ruta pero no se llega al detalle desde el menú, sino seleccionando el post en el listado.

3.10. Añadir al controlador maestro el acceso al detalle

El controlador maestro tiene que proporcionar un método que se encargue de ir al detalle. Ese método tomará como argumento el id del post al que mostrar los comentarios y creará una ruta válida (definida en app.routes.ts) a partir del id.

import { Component, OnInit } from '@angular/core';
import { PostsService } from '../../services/posts.service';
import { Router } from '@angular/router';

@Component({
  selector: 'app-posts',
  templateUrl: './posts.component.html',
  styleUrls: ['./posts.component.css'],
})
export class PostsComponent implements OnInit {
  posts: any[] = [];

  constructor(private postsService: PostsService,
    private router: Router) { (1)
    this.postsService.getPosts().subscribe((data: any) => {
      this.posts = data;
    });
  }

  showComments(id: string) { (2)
    this.router.navigate(['posts', id, 'comments']); (3)
  }

  ngOnInit(): void {}
}
1 Inyección de un objeto Router que permitirá la navegación a la página de detalle
2 Método que implementa el acceso al detalle
3 El método navigate toma como argumentos un array con la lista de literales y/o parámetros a añadir a la URL para confeccionar la ruta.

3.11. Añadir a la vista del maestro el acceso al detalle

Se trata de encerrar entre un hipervínculo el valor del empleado mostrado en el maestro.

  • routerLink le dará formato de hipervínculo.

  • Se creará un evento (click) para llamar al método implementado en el controlador.

<div class="row">
  <div class="col">
    <ul>
      <li *ngFor="let post of posts">
        <a routerLink="" (click)="showComments(post.id)"> (1)
          {{ post.title }}
        </a>
      </li>
    </ul>
  </div>
</div>
1 routerLink hace que tenga estilo de hipervínculo. De la navegación se encarga el método showComments().

El resultado será como el de la figura siguiente.

listadoPostsConLinks

3.12. Implementar el controlador de la parte de detalle

El controlador del detalle tiene que tomar el valor del parámetro pasado en la ruta. Los parámetros se reciben en un array denominado params. Como en el archivo app.routes.ts se definió la ruta posts/:id/comments el parámetro se recibe en params['id'].

import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { PostsService } from '../../services/posts.service';

@Component({
  selector: 'app-comments',
  templateUrl: './comments.component.html',
  styleUrls: ['./comments.component.css'],
})
export class CommentsComponent implements OnInit {
  comments: any[] = []; (1)

  constructor(
    private activatedRoute: ActivatedRoute, (2)
    private postsService: PostsService (3)
  ) {
    this.activatedRoute.params.subscribe((params) => { (4)
      this.postsService.getComments(params['id']).subscribe((data: any) => { (5)
        this.comments = data; (6)
      });
    });
  }

  ngOnInit(): void {}
}
1 Variable de instancia para almacenar los datos recuperados por el servicio
2 Inyección de la ruta para poder acceder al parámetro proporcionado <3 Inyección del servicio para recuperar los detalles del empleado
3 Obtener los parámetros. Se usa subscribe porque se trata de un Observable ya que se trata de una recepción asíncrona.
4 Obtener los comentarios. Se usa subscribe porque se trata de un Observable ya que se trata de una recepción asíncrona.
5 Asignación a una variable de instancia de los datos recibidos en la suscripción asíncrona

3.13. Implementar la vista de la parte de detalle

La vista presenta los valores almacenados en la variable de instancia comments.

<div class="row">
  <div class="col">
    <ul>
      <li *ngFor="let comment of comments">
        <h5>{{ comment.name }}</h5>
        {{ comment.body }}
        <small>
          <span class="badge bg-success">
            {{ comment.email }}
          </span></small
        >
      </li>
    </ul>
  </div>
</div>

El resultado de acceso a un detalle será similar a este.

ComentariosPost

Anexo. Relaciones entre componentes, servicios, módulos and so on

TodoJunto

Si se usan servicios REST, no olvidar importar HttpClientModule en app.module.ts.

...
  imports: [
    BrowserModule,
    RouterModule.forRoot(ROUTES, { useHash: true }),
    HttpClientModule,(1)
  ],
...
1 Importar el provider HttpClientModule