Services y dependency injection: Guía completa – Parte 3

Tercera (¡y última!) parte de esta serie sobre los servicios e inyección de dependencias en Angular. Hasta ahora hemos visto qué son los servicios y hemos dado los primeros pasos para empezar a usarlos. En este post vamos a añadir el toque final para usarlos correctamente. ¡Vamos allá!

Cómo saber cuándo necesitamos una o varias instancias de un mismo Servicio

En la parte #2 habíamos logrado configurar los servicios y aprendido que nuestra app no funciona correctamente porque la instancia del servicio AccountsData, provista en el AppComponent, está siendo sobreescrita por otra instancia del mismo servicio en los dos child components del AppComponent. 

Este el comportamiento actual de Angular. 🐞 Este comportamiento no es un error o bug de Angular, porque habrá circunstancias en las que necesites usar los servicios de esta manera. Sin embargo, para nuestro caso no queremos usarlos así.

Es decir, podemos tener dos necesidades distintas para una app:

  • un Servicio con varias instances
  • un Servicio con una sola instance​​
  • Como digo, para nuestra app necesitamos una sola instancia del servicio AccountsData, pero actualmente estamos usando 3. ¡Sí, 3! 😲 Porque cada vez que proveemos el servicio (en el array "providers"), estamos creando una nueva instancia del mismo.

    Así que para arreglar esta situación, no tenemos más que eliminar 2 de las 3 instancias del servicio, proveyéndolo únicamente en el AppComponent. En el account.component.ts y en el new-account.component.ts, dejamos el array "providers" sólo con el servicio LogService.  

      providers: [LogService]
    

    Este es único cambio que debemos hacer, dejando los constructores tal y como están, ya que desde el constructor estamos informando a Angular de que queremos una instancia de un servicio. Angular ya se encargará de detectar si un componente es un child component, y por tanto, de heredar los servicios del parent component que sean necesarios. 

    Con estos cambios, si ahora guardas y vas a tu navegador, verás que todo funciona correctamente. ¡Genial! 👍

    Cómo inyectar Servicios en otros Servicios 💉

    Tal y como vimos en la parte #2 de esta serie, el nivel más alto donde colocar un servicio es en el AppModule.

    Supongamos que queremos imprimir algo por consola cuando llamamos a un método del AccountsDataService. Es decir, queremos usar el LogService dentro del AccountsDataService. 

    💉 Para poder inyectar un servicio en otro, el primer paso es quitar el AccountsDataService de los providers del app.component.ts y colocarlo en los providers del app.module.tsNo olvides importarlo. 

    import { AccountsDataService } from './accounts-data.service';
    
    @NgModule({
      declarations: [
        AppComponent,
        AccountComponent,
        NewAccountComponent
      ],
      imports: [
        BrowserModule,
        FormsModule,
      ],
      providers: [AccountsDataService],
    

    Con este cambio, nos aseguramos que toda nuestra app reciba la misma instancia del servicio AccountsData. Esto nos permite, entre otras cosas, inyectar ese servicio en otro servicioEsto no sería posible a un nivel inferior (en el AppComponent, donde estaba antes el servicio). 

    1. Hacemos lo mismo con el LogService (lo importamos y declaramos en los providers del AppModule), y lo eliminamos de los providers del AccountComponent NewAccountComponent. Así también tendremos una única instancia de ese servicio a lo largo de toda nuestra app.

      providers: [AccountsDataService, LogService],
    

    2. En el new-account.component.ts, comentamos la llamada del LogService al método logStatusChange. Podemos dejar la inyección del servicio en el constructor, aunque ya no nos hará falta, así que yo la voy a quitar.

      constructor(private accountsDataService: AccountsDataService) { }
    
      onCreateAccount(accountName: string, accountStatus: string) {
        // this.logService.logStatusChange(accountStatus);
        this.accountsDataService.addAcount(accountName, accountStatus);
      }
    

    3. Hacemos lo mismo en el account.component.ts.

      constructor(private accountsDataService: AccountsDataService) { }
    
      onSetTo(status: string) {
        // this.logService.logStatusChange(status);
        this.accountsDataService.updateStatus(this.id, status);
      }
    

    4. En el accounts-data.service.ts, inyectamos el LogService en el constructor y lo importamos.

    5. Llamamos al método del LogService (logStatusChange) dentro de los métodos addAccount updateStatus, pasándole el status y el newStatus como parámetros.

    import { LogService } from './log.service';
    
    export class AccountsDataService {
      accounts = [
        {
          name: 'Master Account',
          status: 'active'
        },
        {
          name: 'Testaccount',
          status: 'inactive'
        },
        {
          name: 'Hidden Account',
          status: 'unknown'
        }
      ];
    
      constructor(private logService: LogService) { }
    
      addAcount(name: string, status: string) {
        this.accounts.push({name: name, status: status});
        this.logService.logStatusChange(status);
      }
    
      updateStatus(id: number, newStatus: string) {
        this.accounts[id].status = newStatus;
        this.logService.logStatusChange(newStatus);
      }
    }
    

    Con estos pasos, podría parecer que ya está todo arreglado, pero nada más lejos de la realidad 😕. Guarda y verás que la consola del navegador te da un error.

    Error: Can't resolve all parameters for AccountsDataService: (?).
    ...

    Lo que ocurre es que,

    siempre que inyectemos un servicio X en algún otro servicio Z, ese servicio Z debe tener algún tipo de "meta data" adjunta. Con "meta data" nos referimos a un decorador y su contenido. 

    Por ejemplo, @Component({...}, @Directive({..}).

    La meta data que debe tener ese servicio Z es el decorador @Injectable()Ese decorador es necesario únicamente en un servicio si queremos que sea susceptible de recibir "una inyección". Es decir, no es necesario añadirlo al servicio que queremos inyectar. Hagamos la prueba, añadiendo el decorador al accounts-data.service.ts.

    import { Injectable } from '@angular/core';
    
    @Injectable()
    export class AccountsDataService {
    

    Si guardas, verás que tu app vuelve a funcionar. ¡Genial! 😺

    👩‍🏫 Sin embargo, Angular prefiere que añadamos el decorador @Injectable() a cualquier servicio, así que para no experimentar problemas inesperados, seguiremos sus recomendaciones, añadiendo el decorador también al LogService.

    import { Injectable } from '@angular/core';
    
    @Injectable()
    export class LogService {
    

    De hecho, verás que si generas automáticamente un servicio con la CLI (ng generate service [nombre servicio]), viene con el decorador incorporado y con un objeto dentro del él:

    import { Injectable } from '@angular/core';
    
    @Injectable({
      providedIn: 'root'
    })
    export class TestService {
    
      constructor() { }
    }
    

    Esta nueva sintaxis nos ahorra tener que proveer cualquier servicio en el AppModule, así que los quitamos de ahí.

    // import { AccountsDataService } from './accounts-data.service';
    // import { LogService } from './log.service';
    
    @NgModule({
      declarations: [
        AppComponent,
        AccountComponent,
        NewAccountComponent
      ],
      imports: [
        BrowserModule,
        FormsModule,
      ],
      // providers: [AccountsDataService, LogService],
    

    🚀 Esta configuración también mejora la velocidad de carga de una app, lo cual se aprecia y es de agradecer en apps mucho más grandes y complejas.

    Cómo usar los Servicios para comunicar componentes entre sí

    Hasta ahora hemos visto cómo los servicios consiguen que nuestro código sea más claro, ligero y fácil de mantener, en comparación con construir una compleja red de comunicación entre componentes, con @Input, @Output, y toda la parafernalia 🤯. 

    Con respecto a nuestra app, veamos cómo afecta la utilización de los servicios en contraposición al uso de una cadena de @Inputs y @Outputs. Para empezar, ya no necesitamos  los custom events "accountAdded" y "statusChanged" en el app.component.html, así que los quitamos.

     <app-new-account></app-new-account>
     <hr>
     <app-account *ngFor="let acc of accounts; let i = index" [id]="i" [account]="acc"> </app-account>

    Imaginemos que, por alguna razón, al hacer clic en alguno de los botones del AccountComponent, queremos mostrar algo en el NewAccountComponent. Sin la intervención de nuestros queridos servicios, tendríamos que dar los siguientes pasos para conseguir eso:

    1º. Emitir un custom event en el AccountComponent

    2º. Capturar el custom event en el selector <app-account> del app.component.html.

    3º. Pasar el dato recién obtenido en el <app-account>, usando property binding, al componente desde el cual queremos gestionar ese dato.

    Un pelín complejo de más, y nada conveniente. La vida es más fácil con los Servicios 😍. Apliquémoslos entonces. Nuestro objetivo es crear un evento que se dispare en un componente X y pueda ser escuchado desde un componente Z. Es un evento que muestra simplemente el nuevo status de una cuenta.

    1. Creamos una propiedad en el AccountsDataService, que será nuestro custom event, así que creamos un event emitter de tipo string.

      statusUpdated = new EventEmitter<string>();
    

    2. En el método onSetTo del account.component.ts, llamamos a la propiedad statusUpdated del AccountsDataService y emitimos el parámetro status. 

      onSetTo(status: string) {
        // this.logService.logStatusChange(status);
        this.accountsDataService.updateStatus(this.id, status);
        this.accountsDataService.statusUpdated.emit(status);
      }
    

    Existe otra manera muy conveniente de emitir eventos, y es usando Observables. Pero eso es materia de otro post, que estará disponible próximamente 🤖.

    Vamos a escuchar nuestro evento desde el NewAccountComponent. Cuando lo escuchemos, vamos a mostrar una alerta (con alert("...") ).

    3. Desde el constructor del new-account.component.ts accedemos al event emitter (statusUpdated) y nos suscribimos a él. Dentro del método subscribe, le pasamos el nuevo estado y configuramos la alerta.

      constructor(private accountsDataService: AccountsDataService) {
        this.accountsDataService.statusUpdated.subscribe((status: string) => alert('The new status is ' + status));
      }
    

    ¡Y ya lo tenemos! 🥳 Si ahora hacemos clic en cualquiera de los botones para cambiar el estado, éste cambiará y su vez nos abrirá una ventana de alerta.

    THE END!

    ¡Y con esto terminamos nuestra serie sobre los Services y la Inyección de dependencias! Espero que hayas aprendido algo nuevo 😊.  Si te queda alguna duda, ¡nos vemos en los comentarios!

    Ejercicio gratis

    (+ sorpresa)

    portada PDF PRACTICA S y DI

    La práctica hace al maestro

    ​

    Escribe tu email aquí abajo y te mandaré un ejercicio que te ayudará a convertirte en todo un pro de los Servicios y la Inyección de dependencias.

    Sobre la autora de este post

    Soy Rocío, una abogada reconvertida en programadora. Soy una apasionada de aprender cosas nuevas y ferviente defensora de que la única manera de ser feliz es alcanzando un equilibrio entre lo que te encanta hacer y lo que te saque de pobre. Mi historia completa, aquí. 

    Más recursos de aprendizaje

    En mi experiencia, la manera más eficaz para aprender Angular es combinando varias vías de aprendizaje. Uno de mis métodos favoritos son los vídeo-cursos y mi plataforma predilecta para eso es Udemy. He hecho varios cursos pero sólo recomiendo aquellos que verdaderamente me han sido útiles. Aquí van:

    angular the complete guide - curso Max S.

      Max Schwarzmüller

    curso angular fernando herrera

       Fernando Herrera

    Si necesitas apoyo en forma de libro, puede que éstos te sirvan de ayuda:

    libro 1 angular
    libro 2 angular

    La programación es un mundo que evoluciona a una velocidad de vértigo. Los autores de estos libros lo saben, por eso suelen encargarse de actualizar su contenido regularmente. Asegúrate de que así sea antes de adquirirlos 😌.

    Participo en el programa de afiliados de Udemy y Amazon, lo que significa que, si compras alguno de estos cursos y/o libros, yo me llevaré una pequeña comisión y a ti no costará nada extra. Vamos, lo que se dice un win-win ðŸ˜Š.

    Otros artículos que pueden interesarte

    Guía de iniciación al data binding en Angular
    ¿Qué es el databinding?El databinding es la forma que tiene Angular para permitirnos mostrar contenido dinámico en lugar de estático (en inglés, hardcoded). Podríamos[...]
    Días del 353 al 386
    Objetivos versus realidad Y nuevamente, llegó otro día clave. Llegó…y pasó. El pasado 4 de marzo este Reto Computer Geek[...]
    Angular: Entendiendo la Directiva ngModel
    Angular es un framework que nos permite, entre otras cosas, añadir contenido dinámico a nuestros archivos HTML. Una de las formas[...]
    Si crees que este post puede serle útil a alguien, por favor, ¡compártelo!:

    Deja un comentario

    Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.

    Como toda web legal que se precie, utilizamos cookies para asegurar que damos la mejor experiencia al usuario en nuestro sitio web. Si continúas utilizando este sitio asumiremos que estás de acuerdo. más información

    Los ajustes de cookies de esta web están configurados para "permitir cookies" y así ofrecerte la mejor experiencia de navegación posible. Si sigues utilizando esta web sin cambiar tus ajustes de cookies o haces clic en "Aceptar" estarás dando tu consentimiento a esto.

    Cerrar