Criando um Ajax Loading no Angular usando Http e Observables

Quando fazemos uma requisição HTTP assíncrona em JavaScript (Ajax) é interessante dar um feedback visual, uma vez que que o navegador não dá nenhum. Aliado a isso, pode ser interessante também bloquear a tela para uso enquanto a requisição não retorna, evitando que o usuário clique em alguma coisa indesejada nesse período.

Nesse post vou demonstrar como fazer um Ajax Loading (aquela janelinha que geralmente diz “Carregando…“, “Loading…“, ou exibe um GIF girando) no Angular 2+. A solução apresentada evita que os pedidos de mostrar/esconder o Loading fiquem espalhados por diversos pontos do código, atuando diretamente na camada de chamada ao serviço Http, sem poluir o código de cada tela/componente. É possível que já exista alguma biblioteca ou componente pronto para isso, mas achei muito interessante registrar a solução pura pois ela aborda decisões de design de classes que podem inspirar a resolução de outros problemas parecidos.

Em linhas gerais, a idéia é simples:

  • Criar uma classe wrapper em torno da classe Http do Angular.
  • Criar um LoadingService cujo estado controla quando o Loading deve ser exibido ou escondido.
  • Criar um recurso visual no template raiz da aplicação que só é exibido quando o LoadingService indica positivo.

Existem outras abordagens possíveis para resolver essa situação, e elas serão discutidas no final do artigo.

Primeiramente vamos construir nosso LoadingService:

import { Injectable } from '@angular/core'

@Injectable()
export class LoadingService {

    private loading: number = 0;

    showLoading() {
        this.loading++;        
    }

    hideLoading(){
        this.loading--;
        if(this.loading < 0){
            this.loading = 0;
        }
    }

    isLoading(){
        return this.loading > 0;
    }
}

No código acima, basicamente, sempre que alguém chama showLoading(), incrementa-se 1 unidade numa propriedade numérica privada. Quando alguém chama hideLoading(), subtraímos 1 unidade. Essa estratégia foi feita pois podem ocorrer diversas requisições HTTP assíncronas, logo, o Loading deve ser exibido enquanto todas elas não terminarem, ou seja, quando esse número volta a ser zero. O método isLoading() permite observar se está ou não acontecendo alguma chamada.

Agora vamos trabalhar na nossa classe wrapper do Http. Um wrapper, como o nome já indica, é um Padrão de Projeto onde uma classe embrulha um objeto, permitindo adicionar tratamentos a esse objeto. O Angular já fornece através do módulo @angular/http uma classe para o programador fazer requisições HTTP do tipo GET/POST/PUT/DELETE, geralmente utilizado para fazer chamadas à API REST que dá suporte à aplicação. Essas chamadas na verdade retornam Observables, que são objetos que permitem ao chamador do método programar o que vai acontecer quando a chamada assíncrona termina. Vamos criar um wrapper chamado HttpService que embrulha o Http do Angular. Veja como fica o código:

import {Injectable} from '@angular/core';
import {Http, Response} from '@angular/http';
import {Observable} from 'rxjs/Rx';
import 'rxjs/add/operator/do';

import { LoadingService } from './loading.service';

@Injectable()
export class HttpService {

    constructor( private loadingService: LoadingService, private http: Http ) { }

    get(url: string): Observable<Response> {
        return this.watch(this.http.get(url));
    }  
    
    //fazer também para o post, put, delete ...

    private watch(response: Observable<Response>): Observable<Response> {

        this.loadingService.showLoading();

        return response.do(
            next => {
                this.loadingService.hideLoading();
            }, 
            error => {   
                //faça aqui seu tratamento de erro
                this.loadingService.hideLoading();
            }            
        );
    }
}

Observe no código acima que o tanto o Http quanto o LoadingService estão sendo injetados no HttpService. A classe deve oferecer métodos para GET/POST/PUT/DELETE, e esses métodos por sua vez delegam a chamada para os respectivos métodos do serviço Http do Angular. No entanto, perceba que quando vamos chamar o método get() do Http por exemplo, na verdade apenas passamos como parâmetro sua chamada para a função watch que irá adicionar um tratamento: antes de executar a chamada pedimos ao LoadingService para exibir o Loading; depois que a chamada é executada, seja com sucesso ou erro, perdimos para esconder o Loading. Esse tipo de chamada pode parecer estranho para quem não está habituado com a parte funcional do JavaScript, já que na verdade não estamos executando método algum, mas apenas anexando coisas à uma chamada que só será feita no futuro, quando de fato alguma código quiser fazer uma chamada a uma URL.

Agora falta preparar o tempalte raíz da aplicação para ter algum elemento visual que será exibido sempre que o LoadingService indicar que tem requisições. Para isso, basta incluirmos alguns controles no componente raiz da aplicação Angular. Veja o código abaixo:

import { LoadingService } from './loading.service';
import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  template: `
    <div id="loading" [class.hidden]="!loading">Loading...</div>
    <div>
      <h1>Angular Loading Test</h1>

      <child-component></child-component>

      <footer>by Rafael Odon (rafaelodon.com)</footer>
    </div>
  `,
  styles: [`
    .hidden{
      display: none;
    }

    #loading {      
      position: fixed;                  
      top: 0px;
      left: 0px;
      width: 100%;
      height: 100%;                           
      color: red;
      background-color: black;
      opacity: 0.5;
      font-size: 5em;      
      text-align: center;            
    }    
  `]
})
export class AppComponent {

  constructor( private loadingService: LoadingService ){}

  get loading(){
    return this.loadingService.isLoading();
  }
}

Observe primeiro o código da classe do componente e ignore o código do template HTML. A classe AppComponent injeta o LoadingService. Definimos a propriede chamada loading, que na verdade não é um atributo interno, mas apenas a definiçaõ de um get. Esse get na prática delega a obtenção dessa informação para o método isLoading() do nosso LoadingService. Com isso, tornamos possível que no nosso template HTML seja possível observar o estado do LoadingService.

No template HTML do componente raiz, basicamente você precisa criar um elemento visual que só é exibido se a propriedade loading do componente for verdadeira. Isso pode ser feito de várias formas. Eu fiz uma DIV que ganha a classe .hidden sempre que a propriedade loading é falsa. No CSS, defini que a classe .hidden esconde o elemento. Ademais, personalizei essa DIV no CSS para que ela ocupe toda a tela, com uma opacidade de 50%, bloqueando o usuário de clicar em outras coisas.

Com essa estrutura montada, qualquer código que que acione o HttpService, direta ou indiretamente, por consequência fará com que o Loading apareça. Veja o código abaixo um componente filho que foi criado para exemplo:

import { HttpService } from './http.service';
import { LoadingService } from './loading.service';
import { Component } from '@angular/core';

@Component({
  selector: 'child-component',
  template: `
    <button (click)="onClick()">Perform a slow request...</button>
    <p>{{message}}</p>
  `  
})
export class ChildComponent {
  
  message:String = "";

  constructor( private httpService: HttpService ){}

  onClick(){
    this.message = "";
    this.httpService.get("http://www.fakeresponse.com/api/?sleep=1").subscribe(
      next => this.message = "Done!"
    );
  }
}

Por fim, vamos ver como fica a configuração do módulo de aplicação:

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

import { HttpService } from './http.service';
import { LoadingService } from './loading.service';

import { AppComponent } from './app.component';
import { ChildComponent } from './child.component';

@NgModule({
  declarations: [
    AppComponent,
    ChildComponent    
  ],
  imports: [
    BrowserModule,
    HttpModule
  ],
  providers: [    
    HttpService,
    LoadingService
  ],
  bootstrap: [AppComponent]
})
export class AppModule {

}

Perceba que estamos importando o módulo HttpModule, já que, no fundo no fundo, as chamadas HTTP da aplicação passam por esse recurso nativo do Angular. Mas estamos provendo para toda a aplicação o HttpService. A idéia então é, quando for necessário fazer uma chamada assíncrona HTTP que exiba o Loading, utilize o HttpService ao invés do Http nativo do Angular.

Alguns podem questionar se não seria possível herdar o Http ao invés de injetá-lo dentro de um wrapper. É uma questão de escolha. Mas ao usar herança será preciso configurar no Angular a sua sub-classe criada como uma alternativa a ser provida no lugar da outra. Uma outra alternativa que tem sido muito apresentada para resolver problemas parecidos é o uso de Interceptors HTTP, mas no entanto isso faz com o que o tratamento dado na interceptação da requisição HTTP sempre aconteça, exigindo algumas manobras para torná-lo condicional. Ao resolver o Loading via wrapper, além de simples, fica mais fácil para o programador decidir quando usar ou não os tratamentos criados.

Um detalhe importante dessa solução é que o Angular provê a mesma instância do LoadingService para toda a aplicação. Como declaramos o LoadingService no módulo da aplicação, ele funcionará como um singleton, conforme é explicado na própria documentação. Isso garante que o estado desse serviço (o número interno que incrementa e decrementa) seja compartilhado e acessado de qualquer ponto da aplicação.

O código completo pode ser visto em: https://github.com/rafaelodon/angular-loading