Como construir um componente para notificações - Angular

Como construir um componente para emitir notificações na tela do usuário

Como construir um componente para notificações - Angular

O objetivo deste artigo é mostrar como podemos construir um mecanismo para emitir notificações ao usuário.

Este mecanismo será composto por uma service e um componente. A ideia é que a service tenha a propriedade providedIn com o valor root em seu decorator para que haja uma única instância em toda a aplicação. Isso facilita bastante visto que a aplicação toda terá acesso a este única instancia que fará a ponte entre a aplicação toda e o componente de notificação.

Já o componente, que chamaremos de app-notificacoes,  será referenciado dentro do template do app.component.html para que as notificações apareçam em todas as telas, visto que nosso <router-outlet> está neste template (app-component.html).

Construção da Service

Aqui não tem muito segredo, vamos estruturar a service da seguinte forma:

import { Injectable } from '@angular/core';
import { Observable, Subject } from 'rxjs';
import { Notificacao, NotificacaoAcao } from './notificacoes.component';

@Injectable({ providedIn: 'root' })
export class NotificacoesService {
    notificacoes = new Subject<NotificacaoAcao>();

    getNotificacoes(): Observable<NotificacaoAcao> {
        return this.notificacoes.asObservable();
    }

    notificar(mensagem: string,
              tempoDeVidaSegundos?: number) {

    }

    removerNotificacao(notificacao: Notificacao) {

    }

    removerNotificacoes() {

    }
}
notificacoes.service.ts

O esqueleto da service está logo acima mas antes de "rechearmos" os métodos, vamos comentar um pouco sobre o Subject<NotificacaoAcao>.

O Subject nos permite emitir valores, que no nosso caso será um objeto do tipo NotificacaoAcao, e quem efetuar uma subscrição (subscribe) irá "ouvir/receber" estes valores. É mais ou menos quando você liga uma rádio no seu carro, a partir do momento que você sintonizou ("subscrição") a estação você passa a receber o audio.

A princípio quem irá "ouvir" estes valores emitidos será o app-notificacoes que será construído mais a frente. Por enquanto vamos finalizar a construção da service e na sequência comentamos sobre os tipos Notificacao e NotificacaoAcao:

import { Injectable } from '@angular/core';
import { Observable, Subject, timer } from 'rxjs';
import { Notificacao, NotificacaoAcao } from './notificacoes.component';

@Injectable({ providedIn: 'root' })
export class NotificacoesService {
    notificacoes = new Subject<NotificacaoAcao>();

    getNotificacoes(): Observable<NotificacaoAcao> {
        return this.notificacoes.asObservable();
    }

    notificar(mensagem: string,
              tempoDeVidaSegundos?: number) {

        const objNotificacao: Notificacao = {
            mensagem,
            tempoDeVidaSegundos,
        };

        this.notificacoes.next({
            acao: 'novo',
            notificacao: objNotificacao,
        });

        if (objNotificacao.tempoDeVidaSegundos) {
            // Multiplicamos os segundos por 1000
            // para obter milisegundos:
            timer(objNotificacao.tempoDeVidaSegundos * 1000)
                .subscribe(() => {
                    this.removerNotificacao(objNotificacao);
                });
        }
    }

    removerNotificacao(notificacao: Notificacao) {
        this.notificacoes.next({
            acao: 'remover',
            notificacao
        });
    }

    removerNotificacoes() {
        this.notificacoes.next({
            acao: 'remover-todas',
        });
    }
}
notificacoes.service.ts

Veja nos métodos acima que estamos emitindo alguns valores através do this.notificacoes.next(...). A ideia é que o nosso componente, que será construído a seguir, receba estas informações e renderize na tela as mensagens de notificação.

Repare que há uma propriedade chamada acao, que será interpretada pelo componente app-notificacoes que veremos mais a frente.

O timer() utilizado dentro do método notificar() é como um setTimeout, ele emite um valor no subscribe sempre que o tempo passado como parâmetro é esgotado, por exemplo:

console.log('teste');

timer(3000).subscribe(() => {
  console.log('Se passaram 3 segundos após o último console.log');
});

Utilizamos este recurso para as notificações que tem tempo de vida, ou seja, podemos emitir uma notificação já configurada para desaparecer após x segundos.

Construção do componente app-notificacoes

Primeiramente vamos criar dois type, um para representar o objeto de notificação e outro para representar uma ação:

export type NotificacaoAcao = {
  acao: 'novo'|'remover'|'remover-todas',
  notificacao?: Notificacao,
};

export type Notificacao = {
    mensagem: string,
    tempoDeVidaSegundos?: number  
}
notificacoes.component.ts

Veja que teremos 3 possíveis cenários com o NotificacaoAcao:

  • incluir uma nova notificação: { acao: 'novo' }
  • remover uma notificação específica: { acao: 'remover' }
  • remover todas as notificações: { acao: 'remover-todas' }

Estas 3 opções serão interpretadas dentro do componente app-notificacoes.

Agora focando no código do componente, temos que injetar a instância da service NotificacosService no constructor, efetuar um subscribe no observable que emite as notificações da service e desenvolver a lógica para os três casos citados logo acima.

O código do componente ficará da seguinte forma:

import { Component } from '@angular/core';
import { Observable, Subscription, timer } from 'rxjs';
import { NotificacoesService } from './notificacoes.service';

export type NotificacaoAcao = {
  acao: 'novo'|'remover'|'remover-todas',
  notificacao?: Notificacao,
};

export type Notificacao = {
    mensagem: string,
    tempoDeVidaSegundos?: number  
}

@Component({
  selector: 'app-notificacoes',
  styleUrls: ['./notificacoes.component.css'],
  templateUrl: './notificacoes.component.html',
})
export class NotificacoesComponent {
  notificacoes: Notificacao[] = [];

  constructor(private notificacoesService: NotificacoesService) {
     this.notificacoesService.getNotificacoes()
      .subscribe((notificacaoAcao: NotificacaoAcao) => {

        switch(notificacaoAcao.acao) {
          case 'novo':
            this.notificacoes.push(notificacaoAcao.notificacao);
            break;

          case 'remover':
            this.notificacoes = this.notificacoes.filter(notificacao => {
              return notificacao !== notificacaoAcao.notificacao;
            });
            break;

          default:
            this.notificacoes = [];
            break;
        }
      });
  }

  remover(notificacao: Notificacao) {
    this.notificacoesService
      .removerNotificacao(notificacao);
  }
}
notificacoes.component.ts

Veja que temos uma variável notificacoes: Notificacoes[] que utilizaremos como referência para renderizar as notificações no template.

O restante do código é apenas para gerenciar o conteúdo deste array a partir das mensagens que chegam no observable da service NotificacoesService.

Código do template e na sequência do CSS:

<div>
  <div *ngFor="let notificacao of notificacoes" 
      class="notificacao">
    <div>{{notificacao.mensagem}}</div>
    <div (click)="remover(notificacao)">x</div>
  </div>
</div>
notificacoes.component.html
:host {
  bottom: 0;
  position: fixed;
  right: 0;
  top: 0;
  width: 300px;  
}

.notificacao {
  background: #efefef;
  border-radius: 8px;
  color: #333;
  display: flex;
  margin: 8px;
  padding: 24px; 
}

.notificacao > div:nth-child(1) {
  flex-grow: 1;
}

.notificacao > div:nth-child(2) {
  cursor: pointer;
}
notificacoes.component.css

Testando as notificações

Agora com o componente pronto, vamos referenciá-lo no app.component.html e adicionar alguns pontos de teste conforme o código abaixo:

<button (click)="notificar()">Notificar</button>
<button (click)="removerNotificacoes()">Limpar Notificações</button>
<app-notificacoes></app-notificacoes>
app.component.html
import { Component } from '@angular/core';
import { NotificacoesService } from './notificacoes/notificacoes.service';

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: [ './app.component.css' ]
})
export class AppComponent  {

  constructor(private notificacoesService: NotificacoesService) {

  }

  notificar() {
    this.notificacoesService.notificar(
      `Teste: ${(new Date())}`,
      this.numeroAleatorio(),
    );
  }

  removerNotificacoes() {
    this.notificacoesService.removerNotificacoes();
  }

  numeroAleatorio(min: number = 1, max: number = 5) {
    return Math.floor(Math.random() * max) + min;
  }
}

Resultado:

Exemplo das notificações sendo apresentadas na tela
Exemplo das notificações sendo apresentadas na tela

Quantidade de notificações

Para aproveitarmos este código e irmos um pouco além do objetivo deste artigo, vamos incrementar um pouco e mostrar no app.component.html a quantidade de notificações na tela. Para isto vamos incluir um novo Subject<number> e deixar o app-notificacoes "avisar" a NotificacoesService sobre a quantidade de visiveis notificações na tela:

import { Injectable } from '@angular/core';
import { Observable, Subject, timer } from 'rxjs';
import { Notificacao, NotificacaoAcao } from './notificacoes.component';

@Injectable({ providedIn: 'root' })
export class NotificacoesService {
    notificacoesLength = new Subject<number>();
    notificacoes = new Subject<NotificacaoAcao>();

    getNotificacoes(): Observable<NotificacaoAcao> {
        return this.notificacoes.asObservable();
    }

    // Obsersable que irá emitir os valores
    // da quantidade de notificações visíveis na tela
    getNotificacoesLength(): Observable<number> {
        return this.notificacoesLength.asObservable();
    }

    // Este método será utilizado pelo app-notificacoes
    // para "avisar" esta service sobre a nova quantidade
    // de notificações
    setNotificacoesLength(quantidade: number) {
        this.notificacoesLength.next(quantidade);
    }

    notificar(mensagem: string,
              tempoDeVidaSegundos?: number) {

        const objNotificacao: Notificacao = {
            mensagem,
            tempoDeVidaSegundos,
        };

        this.notificacoes.next({
            acao: 'novo',
            notificacao: objNotificacao,
        });

        if (objNotificacao.tempoDeVidaSegundos) {
            timer(objNotificacao.tempoDeVidaSegundos * 1000)
                .subscribe(() => {
                    this.removerNotificacao(objNotificacao);
                });
        }
    }

    removerNotificacao(notificacao: Notificacao) {
        this.notificacoes.next({
            acao: 'remover',
            notificacao
        });
    }

    removerNotificacoes() {
        this.notificacoes.next({
            acao: 'remover-todas',
        });
    }
}
notificacoes.service.ts
import { Component } from '@angular/core';
import { Subscription } from 'rxjs';
import { NotificacoesService } from './notificacoes.service';

export type NotificacaoAcao = {
  acao: 'novo'|'remover'|'remover-todas',
  notificacao?: Notificacao,
};

export type Notificacao = {
    mensagem: string,
    tempoDeVidaSegundos?: number  
}

@Component({
  selector: 'app-notificacoes',
  styleUrls: ['./notificacoes.component.css'],
  templateUrl: './notificacoes.component.html',
})
export class NotificacoesComponent {
  notificacoes: Notificacao[] = [];
  subscricao: Subscription;

  constructor(private notificacoesService: NotificacoesService) {
     this.subscricao = this.notificacoesService.getNotificacoes()
      .subscribe((notificacaoAcao: NotificacaoAcao) => {

        switch(notificacaoAcao.acao) {
          case 'novo':
            this.notificacoes.push(notificacaoAcao.notificacao);
            break;

          case 'remover':
            this.notificacoes = this.notificacoes.filter(notificacao => {
              return notificacao !== notificacaoAcao.notificacao;
            });
            break;

          default:
            this.notificacoes = [];
            break;
        }

        // Alteração:
        this.notificacoesService
          .setNotificacoesLength(this.notificacoes.length);
      });
  }

  remover(notificacao: Notificacao) {
    this.notificacoesService
      .removerNotificacao(notificacao);
  }
}
notificacoes.component.ts

Finalmente no AppComponent:

import { Component } from '@angular/core';
import { Observable } from 'rxjs';
import { NotificacoesService } from './notificacoes/notificacoes.service';

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: [ './app.component.css' ]
})
export class AppComponent  {

  notificacoesLength$: Observable<number>;

  constructor(private notificacoesService: NotificacoesService) {
    this.notificacoesLength$ = this.notificacoesService
      .getNotificacoesLength();
  }

  notificar() {
    this.notificacoesService.notificar(
      `Teste: ${(new Date())}`,
      this.numeroAleatorio(),
    );
  }

  removerNotificacoes() {
    this.notificacoesService.removerNotificacoes();
  }

  numeroAleatorio(min: number = 1, max: number = 5) {
    return Math.floor(Math.random() * max) + min;
  }
}
app.component.ts
<h1>Notificações ({{(notificacoesLength$ | async) || '-'}})</h1>
<p>
  Como construir um componente para notificações em Angular.
  <a href="http://consolelog.com.br">consolelog.com.br</a>
</p>

<button (click)="notificar()">Notificar</button>
<button (click)="removerNotificacoes()">Limpar Notificações</button>
<app-notificacoes></app-notificacoes>
app.component.html

Veja que ao invés de efetuarmos um subscribe explicíto no getNotificacoesLength(), estamos utilizando um | async para fazer este trabalho. O código fica bem mais limpo desta forma.

O resultado final:

Exemplo das notificações sendo apresentadas na tela
Exemplo das notificações sendo apresentadas na tela

Considerações

Este artigo apresenta uma forma bem legal de se compartilhar dados entre componentes com a utilização de services com o providedIn: 'root'. Vale lembrar que existem outros métodos para isto e também para construir este esquema de notificação.

Podemos utilizar este recurso do Subject para por exemplo "avisar" a aplicação toda que o usuário efetuou o login ou alterou algo em algum lugar que poderá refletir em outro.

Código completo e funcional:

angular2-componente-notificacoes - StackBlitz
Starter project for Angular apps that exports to the Angular CLI