En las buenas prácticas de Angular se recomienda liberar a los componentes de cualquier lógica no relacionada con la vista. Debes mover toda esa lógica a un servicio.

Cuando trabajas con servicios que ya existen (por ejemplo el servicio Http) todo es muy bonito. Pero cuando creas tus propios servicios… eso es diferente.

De entrada parece sencillo, pero pronto te darás cuenta de que para pasar datos del servicio hacia el componente, necesitas entender bien RxJS. Y es que la programación reactiva está fuertemente recomendada por Angular: Los Observables son el compañero de viaje habitual de los servicios.

La cuestión es esta: Es probable que la lógica que quieres mover al servicio, emita algún evento de salida (EventEmitter).

El problema es que EventEmitter no está pensado para servicios. Lo ideal es que la API del servicio disponga de un Observable donde se transmita el evento. Así te podrás suscribir desde el componente.

Entendiendo el problema

Para transmitirte mejor esta problemática te voy a poner un ejemplo muy simple (en Angular 4): Tengo un contador que realiza una cuenta atrás, es extremadamente sencillo.

timer

El componente en sí tiene una entrada init con los segundos iniciales y una salida onComplete para avisar que ha completado la cuenta atrás.

Se usa así:

<app-timer init="5" (onComplete)="logCompleted()"></app-timer>

He movido la lógica del temporizador a un servicio, pero me falta emitir ese evento onComplete. Mi componente ahora mismo tiene esta pinta:

//src/app/timer/timer.component.ts

//...some imports...

@Component({
  selector: 'app-timer',
  templateUrl: './timer.component.html',
  providers: [TimerService]
})
export class TimerComponent implements OnInit, OnDestroy {

  @Input() init:number = 0;
  @Output() onComplete = new EventEmitter<void>();
  //TODO: emit event when count ends with this.onComplete.emit();


  constructor(public timer:TimerService){}

  ngOnInit(){
    this.timer.restartCountdown(this.init);
  }

  ngOnDestroy(){
    this.timer.destroy();
  }

}

El servicio por su parte es también muy simple y solo contiene la lógica del temporizador. Te muestro su estructura para que te sitúes:

// src/app/timer/timer.service.ts

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


@Injectable()
export class TimerService {

  private countdownTimerRef:any = null;
  private init:number = 0;
  public countdown:number = 0;

  constructor() { }

  public restartCountdown(init?){
      //restart the countdown
  }

  public destroy(){
      //clean timeout reference
  }

  private doCountdown(){
    //call process countdown after 1 second
  }

  private processCountdown(){
    //check if countdown has finished
    //HERE I SHOULD EMIT THE EVENT
  }

  private clearTimeout(){
    //remove countdown reference
  }

}

Puedes ver que hay un método que comprueba si la cuenta atrás ha acabado (processCountdown). Ahí es donde necesito emitir el evento.

Parece bastante claro que si el servicio tiene un objeto Observable, podrás suscribirte en el componente. Así, al acabar la cuenta atrás puedes emitir un evento desde el servicio, detectarlo en el componente y usar ahí el EventEmitter.

¿Como generar eventos en un Observable?

La clave aquí es la clase Subject.

Los Subjects son Observables que además pueden manejar múltiples suscripciones a un único flujo y son capaces de emitir eventos.

Como los eventos solo los quieres generar a nivel interno, lo que debes hacer es crear un Subject privado, y exponer un Observable público con el flujo del primero.

// src/app/timer/timer.service.ts

import { Injectable } from '@angular/core';
import { Subject } from "rxjs/Subject";


@Injectable()
export class TimerService {

  //...other properties... 

  private countdownEndSource = new Subject<void>();
  public countdownEnd$ = this.countdownEndSource.asObservable();

  //...methods...
}

Para emitir un nuevo valor en el flujo de datos que maneja el Observable, tienes que usar el método next del Subject.

Esto es justo lo que hago en el método processCountdown de mi servicio, cuando la cuenta atrás llega a cero:

// src/app/timer/timer.service.ts

//...imports...

@Injectable()
export class TimerService {

  //...other stuff...

  private processCountdown(){
    if(this.countdown == 0){
      this.countdownEndSource.next();
    }
    else{
      this.doCountdown();
    }
  }

Suscribirse a un observable

El resto es coser y cantar y seguro que ya lo has hecho alguna vez.

Desde el componente lo que tienes que hacer es suscribirte a ese observable y actuar cuando recibas el evento.

No olvides cancelar la suscripción al destruir el componente. Para eso, deberás obtener una referencia a la suscripción con un objeto del tipo Subscription.

Así es como queda mi componente:

// src/app/timer/timer.component.ts

import { Component, OnInit, OnDestroy, Input, Output, EventEmitter } from '@angular/core';
import { TimerService } from "app/timer/timer.service";
import { Subscription } from "rxjs/Subscription";

@Component({
  selector: 'app-timer',
  templateUrl: './timer.component.html',
  providers: [TimerService]
})
export class TimerComponent implements OnInit, OnDestroy {

  @Output() onComplete = new EventEmitter<void>();
  @Input() init:number = 20;

  private countdownEndRef: Subscription = null;

  constructor(public timer:TimerService){}

  ngOnInit(){
    this.timer.restartCountdown(this.init);

    this.countdownEndRef = this.timer.countdownEnd$.subscribe(()=>{
      this.onComplete.emit();
    })
  }

  ngOnDestroy(){
    this.timer.destroy();
    this.countdownEndRef.unsubscribe();
  }

}

Programación reactiva

Hasta ahora la cuenta atrás la cojo directamente en el template del componente, accediendo a timer.countdown.

Esto, no es muy eficiente, ya que es angular quien todo el rato tiene que comprobar si el valor de countdown ha cambiado. Sería mejor que el propio servicio me avisara cuando el valor ha cambiado. Esto es lo que se conoce como Programación Reactiva.

Los observables combinan especialmente bien con una estrategia de ChangeDetectionPush para reducir el número de comprobaciones que hace Angular.

Esto, si eso, lo explico en detalle otro día 😉

Puedo seguir la misma estructura que antes para que el servicio exponga un Observable con el flujo de la cuenta atrás. El componente a su vez, solo tendrá que suscribirse a este objeto.

OFERTA
Curso

Componentes Angular Nivel PRO

Domina los componentes de Angular (Angular 2/4/5+) como un experto y crea componentes técnicamente brillantes.
Idioma: Español
23 €110 €

Aquí aparece un nuevo problema, claro. Antes tenía un estado permanente. Podía consultar el valor de la cuenta en cualquier momento. En cambio, usando un Subject solo sabría el valor en el momento en que recibo el evento.

Ese problema tiene solución. RxJS proporciona una variante de Subject que justamente sirve a este objetivo, el BehaviorSubject.

Subject VS BehaviorSubject

Un BehaviorSubject es como un Subject, salvo que tiene noción de su estado.

Básicamente se diferencian en que el BehaviorSubject:

  • Siempre tiene un valor (por eso, al crearlo lo tendrás que inicializar).
  • En el momento de la suscripción, recibes el último valor disponible.
  • Puedes obtener su valor en cualquier momento con el método getValue()

Usando BehaviourSubject en un servicio

Voy a enseñarte como reemplazar la propiedad countdown del servicio (que mantiene el estado de cuenta atrás), por un BehaviorSubject.

Lo primero es crear el BehaviorSubject y su Observable.

// src/app/timer/timer.service.ts

//...other imports...
import { BehaviorSubject } from 'rxjs/BehaviorSubject';

@Injectable()
export class TimerService {

  private countdownSource = new BehaviorSubject<number>(0);
  public countdown$ = this.countdownSource.asObservable();

 //...more stuff..

}

Como ves, al crear el BehaviorSubject le tengo que pasar un valor para inicializarlo.

Vamos con el método restartCountdown. Donde antes actualizaba el valor inicial haciendo this.countdown = this.init, ahora lo hago con this.countdownSource.next(this.init).

// src/app/timer/timer.service.ts

//...

  restartCountdown(init?){
    if(init)
      this.init = init;

    if(this.init && this.init >0){
      this.clearTimeout();
      this.countdownSource.next(this.init);
      this.doCountdown();
    }
  }

//...

También actualizo el método doCountdown para obtener y decrementar la cuenta con getValue:

// src/app/timer/timer.service.ts

//...

  private doCountdown(){
    if(this.countdownSource.getValue() > 0){
      this.countdownTimerRef = setTimeout(()=>{
        this.countdownSource.next(this.countdownSource.getValue() -1);
        this.processCountdown();
      }, 1000);
    }
  }

//...

Hago lo propio en el método processCountdown para obtener el valor de la cuenta:

// src/app/timer/timer.service.ts

//...

  private processCountdown(){
    if(this.countdownSource.getValue() <= 0){
      this.countdownEndSource.next();
    }
    else{
      this.doCountdown();
    }
  }

//...

¡Listo! Ya solo falta actualizar el componente.

Podría suscribirme como antes al Observable y asignar los resultados a una nueva propiedad que leería desde el template.

Voy a hacer algo mejor: Le voy a ceder todo ese trabajo a una pipe.

AsyncPipe

La AsyncPipe de Angular es una herramienta muy potente cuando trabajas de forma reactiva.

Esta pipe lo que hace es suscribirse a un Observable y devolver el último valor emitido. Además, cuando el componente se destruye, la pipe cancela la suscripción por ti.

Así, el único cambio que necesita mi componente para funcionar con este contador reactivo, es a nivel de template.

Fíjate en como uso la pipe en el binding al input time de <app-display>:

<!-- src/app/timer/timer.component.html -->

<div class="timer">
  <app-display [time]="timer.countdown$ | async"></app-display>
  <button (click)="timer.restartCountdown()">RESTART</button>
</div>

¡Y eso es todo!

Esto y otras muchas buenas prácticas las encontrarás en mi curso Componentes en Angular – nivel PRO

Con estos cambios, he conseguido que el componente pase a usar la cuenta atrás de forma totalmente reactiva, sin necesidad de que Angular compruebe continuamente la variable countdown.

¿Quieres jugar con el código fuente del ejemplo?
Lo tienes en este repositorio.

Conclusiones

Sacar la lógica de negocio del componente, para meterla en un servicio es algo recomendable. Te facilitará la lectura de código y simplificará su testabilidad. Además, como has visto, Angular utiliza herramientas muy potentes como RxJS que te facilitan la programación reactiva, lo que en sí mismo, también es una buena práctica.

Créeme, cuando crezca tu aplicación, agradecerás haber seguido esta aproximación. Tener un buen rendimiento o no, puede depender de ello.