Comunicación entre componente, rutas y servicios en Angular

Este es el segundo tutorial de la serie Fundamentos de Angular donde aprendemos los fundamentos de este fabuloso framework. En este tutorial aprenderemos a utilizar rutas y enviar datos entre componentes para crear aplicaciones aún más avanzadas y complejas con Angular. Al finalizar este tutorial podrás crear una aplicación en Angular que use estas características y consuma datos desde un Api de Rick y Morty.

Índice

  1. Comunicación entre componentes
  2. Manejo de rutas
  3. Rutas con parametros
  4. Los servicios
  5. Formulario de búsqueda
  6. Tu turno
  7. El código

1. Comunicación entre componentes

La comunicación de componentes es una característica muy importante dentro de una aplicación web. Hay aplicaciones en los cuales es necesario que un componente padre envié información hacia un componente hijo para que este pueda mostrar la información con más detalle. También tenemos casos donde un componente hijo necesita enviar información a un componente padre. Los casos de uso de la comunicación entre componentes pueden ser innumerables, así que es necesario estudiarlo y comprenderlos para poder construir aplicaciones realmente potentes. A continuación explicaremos estos dos tipos de comunicación que mencionamos.

De componente padre a hijo

Dentro de nuestro componente hijo necesitamos una librería llamada Input que nos permitirá utilizar un decorator mediante el cual declararemos las variable donde recibiremos los datos enviados por el componente padre.

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

@Component({
    ...
})
export class CardComponent{

    // Declaramos la variable que recibirá la variable enviada por el padre.
    @Input() character:any = {}

    constructor(){

    }

}

En el HTML del componente padre solo tenemos que crear una directiva personalizada del mismo nombre que la variable donde recibiremos los datos en el componente hijo.

<!-- Si queremos enviar variables utilizamso [] -->
<target-compoentne [heroe]="variable" ></targeta-componente>

<!--Si solo quieres enviar un string no se usa []-->
<target-compoentne heroe="text" ></targeta-componente>

De componente hijo a padre

Para esto dentro de nuestro componente hijo utilizaremos dos librerías de Angular Output y EventEmitter. El mensaje será enviado a forma de evento emitido por el componente hijo. En el componente hijo usamos el siguiente código que pasamos a explicar a continuación.

import { Component, Output, EventEmitter } from "@angular/core";

@Component({
  selector: "app-hijo",
  templateUrl: "./hijo.component.html",
})
export class HijoComponent {
  // Creamos la variable a enviar al padre
  @Output() mensaje: EventEmitter<string>;

  constructor() {
    // Inicializamos la emicion de eventos
    this.mensaje = new EventEmitter();
  }
  emitirMensaje() {
    // Usando la variable emitimos el valor que queremos enviar
    this.mensaje.emit("Gaaaaaa Este es el mensaje");
  }
}

Ahora dentro del código HTML del componente Hijo utilizamos el evento click para ejecutar la función emitirMensaje().

<div>
  <h1>Hijo</h1>
  <button (click)="emitirMensaje()">click me</button>
</div>

Ya emitimos el mensaje ahora solo falta que el componente padre lo reciba para esto debemos crear una función dentro del componente padre de la siguiente manera.

msj:string;

recibirMensaje(mensaje:string){
    this.msj = mensaje;
}

Ahora debemos ejecutar esta función pero en el parámetro obtener el evento emitido por el componente hijo, para esto hacemos lo siguiente.

<app-hijo (mensaje)="recibirMensaje( $event )"></app-hijo>

En este código (mensaje) es el nombre de la variable output del componente hijo y $event es la captura del .emit() que enviamos en el componente hijo.

Listo!! Ya tenemos nuestro mensaje guardado en una variable del componente padre msj, el cual podemos usar dentro del HTML y mostrarlo usando {{ }}.

2. Manejo de rutas

Las rutas son una parte importante dentro de un proyecto Angular grande, ya que seguramente esta aplicación deberá manejar múltiples páginas y gracias a las rutas podremos administrar el cambio de ellas utilizando componentes embebidos en un componente general. Utilizando rutas podremos construir verdaderas Single Page Applications.

Para comenzar a utilizar rutas en Angular sin haber instalado Angular Routing, en las opciones de configuración inicial, debemos crear el archivo app/app.routes.ts. Este archivo controlará las rutas de nuestro proyecto, por lo que debe tener un código como el que pasaremos a explicar a continuación.

// 1. Importamos las librerias necesarias para el uso de rutas
import { RouterModule, Routes } from "@angular/router";

// 2. Importamos los componentes para las rutas
import { HomeComponent } from "./components/home/home.component";
import { AboutComponent } from "./components/about/about.component";

// 3. Creamos una constante de tipo Routes que contendrá las rutas
const APP_ROUTE: Routes = [
  // Estas son las rutas de nuestra aplicación
  { path: "home", component: HomeComponent },
  { path: "about", component: AboutComponent },
  // Esta objeto redirecciona a una ruta especifica cuando no encuentra una ruta específica
  { path: "**", pathMatch: "full", redirectTo: "home" },
];

// 4. Exporta una constante creada a partir de RouterModule de las rutas descritas antes
export const APP_ROUTING = RouterModule.forRoot(APP_ROUTE);

Ahora lo que tenemos que hacer es referenciar este archivo de rutas dentro de nuestro app/app.module.ts. Para esto primero tenemos que importar nuestro archivo de rutas y luego agregarlo dentro de la propiedad imports del decorator @MgModule, como en el siguiente ejemplo.

import { APP_ROUTING } from './app.routes';

@NgModule({
  declarations: [
    ...
  ],
  imports: [
    BrowserModule,
    APP_ROUTING
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Ahora solo falta colocar una etiqueta Angular en el HTML de nuestro componente principal App.component.html que funcionará de contenedor de los componentes que cambiarán con las rutas. Esta etiqueta es el <router-outlet></router-outlet>.

Ahora solo falta algún método, como un botón, para poder cambiar de rutas desde el HTML. Para este fin, en Angular existe la directiva routerLink que lo usaremos de la siguiente forma dentro del HTML.

<button [routerLink]="['home']">Home</button>

Además Angular nos brinda una directiva que nos permite colocar una clase a una determinada etiqueta cuando la ruta que abrió está activa. Esta directiva es routerLinkActive que se usa junto con el routerLink de la siguiente forma.

<a routerLinkActive="active" [routerLink]="['home']">Home</a>

Listo!! ya somos capaces de usar rutas accionadas por alguna etiqueta HTML dentro de nuestros proyectos Angular.

3. Rutas con parámetros

Las rutas con parámetro son aquellas en las que dentro de la ruta se envía un parámetro determinado para que el componente lo procese o muestre alguna información relacionada a dicho parámetro. Un ejemplo de ruta con parámetro sería el siguiente.

https://www.miweb.com/personaje/1

Para poder trabajar con este tipo de rutas lo principal es obtener dicha ruta en el componente que este abre, de tal modo realizar alguna lógica con ese parámetro.

Para poder realizar esto tenemos que cambiar un poco la ruta dentro de nuestro archivo de rutas app.routes.ts de tal forma que pueda aceptar algún parámetro. El código de la ruta acepta un parámetro id pero abre un único componente. Ese componente es el que se encargará de obtener el parámetro para procesarlo.

{ path: 'character/:id', component: CharacterComponent }

Ahora para poder obtener la ruta desde un componente tenemos que hacer uso de una librería llamada ActivatedRoute que mediante el constructor de nuestro componente obtendremos la ruta activa y su respectivo parámetro.

import { Component } from '@angular/core';
import { ActivatedRoute } from '@angular/router'

@Component({
    ...
})
export class CharacterComponent {
    constructor( private activatedRoute : ActivatedRoute){
        console.log(activatedRoute);
    }
}

El objeto ActivateRoute contiene información acerca de la ruta que está activa en el navegador. Este objeto tiene la propiedad params que a su vez tiene información sobre los parámetros enviados a la ruta. Sin embargo, si bien podemos ver esta información usando un console.log() no es posible ingresar al valor de la ruta de manera directa.

Si buscamos en la documentación de Angular descubriremos que el params es un objeto de la interface Observable<Params>. Pues bien, la forma de obtener este parámetro será utilizando un método presente en un Observable llamado subscribe al cual enviaremos como parámetro una función flecha para obtener el parámetro como se muestra en el código siguiente.

constructor( private activatedRoute : ActivatedRoute){
    activatedRoute.params.subscribe( prm => {
        console.log(`El id es: ${prm['id']}`);
    })
}

Listo!! Ya sabemos crear rutas con parámetros y recibir el parámetro desde un componente. Ahora lo único que nos falta para terminar esta sección es saber como ingresar a una ruta enviando un determinado parámetro. Para esto existen dos métodos. El primero es desde el HTML y el segundo es desde el TypeScript.

Utilizando el HTML

Para dirigirnos a una ruta desde el HTML es bastante sencillo, solo es necesario enviar un parámetro por la directiva routeLink de la siguiente manera.

<button [routerLink]="['/character', '1']">Click me</button>

Utilizamos / porque queremos que la nueva ruta comience desde la raiz de nuestra web y no de rutaactual/character/1. También debes tener en cuenta que el parámetro es un string.

Utilizando el TypeScript

Ahora para dirigirnos a una ruta desde el TypeScript tenemos que hacer uso de la librería Router en el TypeScript del componente para luego utilizar el método navigate de la siguiente forma.

import { Component } from '@angular/core';
import { Router } from '@angular/router';

@Component({
    ...
})
export class CharactersComponent{

    verPagina(id: number){
        this.router.navigate(['/character', id])
    }

    constructor(private router:Router){

    }

}

Luego solo debemos ejecutar el método verPagina(1) desde alguna etiqueta HTML con la directiva (Click).

Listo!! Ya sabemos todo lo necesario para utilizar rutas con parámetros en nuestros proyectos Angular.

4. Los servicios

Los servicios son muy importantes en Angular, estos nos permiten brindar información al componente que lo requiera. Podemos utilizarlos para mantener la persistencia de datos en nuestra aplicación web o realizar peticiones asíncronas hacia alguna API. Además como son independiente a los componentes son totalmente reutilizables.

Los servicios en Angular tenemos que crearlos dentro de la carpeta src/app/services. Dentro de la carpeta usaremos la convención datos.service.ts para los archivos de los servicios. Dentro de estos archivos crearemos la siguiente estructura de código.

// Importamos la libreria necesaria para que los servicios puedan ser usados en Angular medainte la inyección de dependencias
import { Injectable } from "@angular/core";

// Usamos el decorator que le indica a Angular que esta clase podrá ser injectada en cualquier parte del proyecto
@Injectable()
// Creamos la clase para nuestro servicio
export class DataService {
  constructor() {
    console.log("Servicio listo para usarce");
  }
}

Ahora tenemos que agregar nuestro servicio al archivo app.module.ts para que Angular lo pueda reconocerlo y podamos usarlo en nuestros componentes. Para esto debemos importar el servicio y luego colocarlo en la propiedad providers.

// Importando el servicio
import { HeroesService } from './services/heroes.service';

@NgModule({
  ...
  providers: [
    HeroesService
  ],
  ...
})
export class AppModule { }

Para poder utilizar un servicio dentro de un componente primero debemos importarlo luego debemos declararlo dentro del constructor de la siguiente manera.

import { Component } from '@angular/core';
// Importamos el servicio
import { DataService } from '../../services/data.service';

@Component({
  ...
})
export class CompComponent {

  // Enviamos el servicio como parametro al constructor de nuestro componente
  constructor(private dataService:DataService) {

   }
}

Ahora ya estamos usando el servicio y lo podemos comprobar si dentro del servicio colocamos un console.log() y vemos en la consola lo que sucede cuando ejecutamos el componente. En el siguiente ejemplo, vamos como utilizarlo creando un un arreglo de objetos estático con una interface y obteniendo los datos desde algún componente.

@Injectable()
export class HeroesService {

private heroes:Heroe[] = [
    {
      nombre: "IronMan",
      bio: "Es uno de los Avenger más conocidos",
    },
    {
      nombre: "SpiderMan",
      bio: "Es el increible hombre araña",
    }];

    constructor(){

    }

    public getHeroes():Heroe[]{
      return this.heroes;
    }
}

export interface Heroe{
  nombre: string;
  bio: string;
}
// Importamos el servicio y la interface
import { ServService, Heroe } from '../../services/heroes.service';

// Usamos la interface para crear el arreglo de heres
heroes:Heroe[] = [];

constructor(private _heroesService:HeroesService) {

}
// El OnInit se ejecuta cuando el constructor termina de renderizarse
ngOnInit(): void {
  this.heroes = this._heroesService.getHeroes();
  console.log(this.heroes);
}

Obteniendo datos desde un API público

La mayoría de veces se obtiene la información desde un API en formato json. Así que será más productivo aprender a realizar esto. Lo primero que debemos conocer es que necesitamos realizar peticiones GET Http y para esto Angular tiene la librería HttpClient que es parte del modulo HttpClientModule. Por esta razón tenemos que agregar primero este modulo dentro de nuestro app.module.ts para poder usarlo.

import { HttpClientModule } from '@angular/common/http';

@NgModule({
  declarations: [
    ...
  ],
  imports: [
    ...
    HttpClientModule
  ],
  providers: [
    ...
  ],
  bootstrap: [
    ...
  ]
})
export class AppModule { }

Luego en nuestro servicio implementaremos el método para traer datos desde el API. Para esto usaremos la librería HttpClient de la siguiente forma.

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

@Injectable()
export class DataService{

    public getData(){
        return this.http.get('https://rickandmortyapi.com/api/character/?page=1');
    }

    constructor(private http:HttpClient){

    }
}

Ahora tenemos que utilizar este método en el componente que necesite los datos. Para eso otra vez utilizaremos el método OnInit() donde colocaremos el código que actualizará una variable local que contendrá los datos.

characters:Array<Object> = [];

ngOnInit() {
    this.dataService.getData()
        .subscribe(data => {
            this.characters = data['results'];
    });
}

Ahora ya podemos usar nuestra variable characters desde nuestro HTML para mostrar los datos dentro de nuestra aplicación.

5. Formulario de búsqueda

Si recibimos muchos datos desde un API es posible que se requiera construir una caja de búsqueda o filtro de los resultados por termino. Existen diferentes maneras de afrontar este problema, en aquí analizaremos solo una forma de hacerlo.

El componente Navbar donde se encuentra la caja de búsqueda nos redireccionará hacia una ruta donde enviaremos el termino que buscamos por parámetro. Por lo que necesitamos crear un nuevo componente de búsqueda que obtenga ese parámetro y dentro renderice solo los elementos filtrados del API.

Para ejecutar una función de redireccionamiento con el valor de la caja de búsqueda como parámetro hacemos lo siguiente.

<form>
  <input
    type="search"
    placeholder="Search"
    (keyup.enter)="search(searchText.value)"
    #searchText
  />
  <button (click)="search(searchText.value)" type="submit">Search</button>
</form>

En el código anterior usamos #searchText para obtener una referencia de la etiqueta input de esta forma le pasamos el valor del input a la función que estamos ejecutando con el evento (keyup.enter). Esta referencia también se mantiene en todo el código HTML por lo que también lo usamos en la etiqueta button.

Este input ejecuta la función que nos redirige hacia una ruta determinada enviando como parámetro el termino que buscamos. Así que tenemos que crear las rutas respectivas y el componente asociado a ella. Pasemos a explicar el código del componente.

import { Component, OnInit } from "@angular/core";
import { Router, ActivatedRoute } from "@angular/router";
import { DataService } from "../../services/data.service";

@Component({
  selector: "app-search",
  templateUrl: "./search.component.html",
})
export class SearchComponent implements OnInit {
  // Variable para almacenar el total de personajes
  characters: any = [];
  // Variable para almacenar los personajes filtrados
  charactersFilter: any = [];
  // Variable para almacenar el parametro
  term: string;

  // Inicializo la variables que me permitirá obtener el parametro de la ruta
  // Inicializo el servicio
  constructor(
    private router: Router,
    private activatedRoute: ActivatedRoute,
    private dataService: DataService
  ) {
    // Primero utilizo el route para capturar el evento de cambio de ruta
    this.router.events.subscribe((path) => {
      // Obtendo el parametro de la ruta
      this.activatedRoute.params.subscribe((params) => {
        this.term = params["term"];
      });
      // Obtengo todos los personajes mediante el servicio
      this.dataService.getData().subscribe((data) => {
        // Almaceno los personajes
        this.characters = data["results"];
        // Filtro los personajes en una nueva variable usando la función que creamos
        this.charactersFilter = this.search(this.term);
      });
    });
  }

  // Funcion que filtrará los personajes por el termino buscado
  search(term: string) {
    let characters_aux: any[] = [];
    term = term.toLowerCase();

    for (let item of this.characters) {
      let name = item.name.toLowerCase();
      if (name.indexOf(term) >= 0) {
        characters_aux.push(item);
      }
    }
    return characters_aux;
  }

  ngOnInit() {}
}

Si probamos nuestra aplicación nos daremos cuenta que al momento de enviar los datos del formulario de búsqueda la página se recarga, esto hace nuestra experiencia de navegación muy mala. Para evitar esto tenemos que agregar el modulo FormsModule dentro de nuestro archivo app.module.ts.

import { FormsModule } from '@angular/forms';


@NgModule({
  declarations: [
    ...
  ],
  imports: [
    ...
    FormsModule
  ],
  providers: [
    ...
  ],
  bootstrap: [
    ...
  ]
})
export class AppModule { }

Listo!! Ahora ya tenemos dentro de una variable los personajes filtrados por el termino que escribimos en el cuadro de búsqueda. Solo falta renderizar esos datos en el HTML del componente.

6. Tu turno

Usando estos conceptos que acabamos de conocer de Angular te animo a realizar el siguiente proyecto. Una aplicación para buscar personajes de la famosa serie Rick y Morty. Utiliza componentes, servicios, la comunicación entre componentes y el API publico de Rick y Morty. Puedes probar la aplicación ya construida en Netlify.rick-and-morty-angular.

Cuando quieras puedes ver el código del proyecto terminado. Pero ándale inténtalo tu mismo!!

7. El código

El código del proyecto está en el siguiente repositorio. github.com/andygeek/rick-and-morty-angular