Como construir um Modal - Angular

No mundo do desenvolvimento, eventualmente encontramos mais de uma forma para chegar no mesmo resultado. A construção de um modal não foge da regra. Neste post vamos mostrar como criar um modal bem simples, de única instância e com o animations do Angular para dar um efeito visual interessante.

Desenvolvimento da ideia

Pessoalmente gosto de desenvolver a ideia do componente utilizando simplesmente HTML, CSS e JavaScript. Então recorro ao JSFiddle para estudar o cenário.


Exemplo de utilização do emmet para agilizar o desenvolvimento

Dica: no editor do JSFiddle digite html:5 e aperte tab para o editor criar uma estrutura HTML como a imagem acima mostra. Se quiser saber mais sobre estes atalhos (emmet) acesse este link.


Sem dar muitas voltas, o modal em questão basicamente deve ter sombra, bordas arredondadas e fundo branco. Traduzindo isso para CSS e HTML temos o seguinte código:

.modal {
  background: #fff;
  border-radius: 8px;
  box-shadow: 0 2px 8px #333;
  min-width: 320px;
  position: fixed;
  left: 50%;
  padding: 24px;
  top: 50%;
  transform: translate(-50%, -50%);
}
<div class="modal">
  Lorem ipsum dolor sit amet,
  consectetur adipisicing elit.
  Aliquam, sunt.
</div>
Renderização do modal com base do código logo acima

Veja na imagem acima que já temos um resultado inicial mas ainda precisamos evoluir.

Neste texto nosso foco não é o CSS, mas para não passar batido vale comentar que o left: 50% e o top: 50% posicionam o <div> tendo como referencia (origem) a diagonal superior esquerda, ou seja, essa ponta superior esquerda do modal ficará bem no meio da tela conforme a imagem abaixo mostra:

Renderização do modal sem a propriedade CSS transform

Então adicionamos o translate( -50%, -50%) para recuar no topo e no lado esquerdo. O recuo equivale a 50% do tamanho do próprio div. Desta forma ele fica centralizado na tela. A imagem abaixo ilustra em azul o cenário sem o translate e em vermelho com o translate:

Deslocamento do modal utilizando o translate

Um outro elemento que normalmente todo modal tem, é um fundo mais escuro que cobre a tela toda por baixo do modal, também conhecido como overlay. Para criarmos este efeito podemos utilizar o seguinte código:

.overlay {
  background: #000;
  height: 100vh;
  left: 0;
  opacity: 0.5;
  position: fixed;
  top: 0;
  width: 100vw;
}
<div class="modal">
  Lorem ipsum dolor sit amet,
    consectetur adipisicing elit.
    Aliquam, sunt.
</div>
<div class="overlay"></div>

O resultado ficaria da seguinte forma:

Renderização mostrando que o overlay sobrepõem o modal

Veja que o overlay ficou em cima do modal. Para corrigir basta adicionar o z-index no CSS:

.modal {
  background: #fff;
  border-radius: 8px;
  box-shadow: 0 2px 8px #333;
  min-width: 320px;
  position: fixed;
  left: 50%;
  padding: 24px;
  top: 50%;
  transform: translate(-50%, -50%);
  z-index: 11;
}

.overlay {
  background: #000;
  height: 100vh;
  left: 0;
  opacity: 0.5;
  position: fixed;
  top: 0;
  width: 100vw;
  z-index: 10;
}

Resultado final:

Renderização do modal

Construção do componente no Angular

Agora que montamos uma estrutura HTML + CSS vamos migrar este conteúdo para um componente Angular:

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

@Component({
  selector: 'app-modal',
  templateUrl: 'modal.component.html',
  styleUrls: ['modal.component.css'],
})
export class ModalComponent {
  mostrar: boolean = false;

  toggle () {
    this.mostrar = !this.mostrar;
  }
}
modal.component.ts
.modal {
  background: #fff;
  border-radius: 8px;
  box-shadow: 0 2px 8px #333;
  min-width: 320px;
  position: fixed;
  left: 50%;
  padding: 24px;
  top: 50%;
  transform: translate(-50%, -50%);
  z-index: 11;
}

.overlay {
  background: #000;
  height: 100vh;
  left: 0;
  opacity: 0.5;
  position: fixed;
  top: 0;
  width: 100vw;
  z-index: 10;
}
modal.component.css
<!-- 
     Vamos controlar (mostrar/esconder)
     o modal através deste *ngIf
-->
<ng-container *ngIf="mostrar">
  <div class="modal">
    <!-- 
         No lugar do texto "Lorem ipsum..."
         adicionamos o código abaixo para
         que o usuário do componente possa
         passar o conteúdo para dentro do 
         modal, por exemplo:
         <app-modal>meu conteudo</app-modal> 
     -->
    <ng-content></ng-content>
  </div>
  <div (click)="toggle()"
       class="overlay"></div>
</ng-container>
modal.component.html
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ModalComponent } from './modal.component';

@NgModule({
  declarations: [
    ModalComponent,
  ],
  imports: [
    CommonModule,
  ],
  exports: [
    ModalComponent,
  ]
})
export class ModalModule {

}
modal.module.ts

O primeiro passo para testar o modal dentro do app.component.html é importar o módulo ModalModule dentro do AppModule que é quem declara o AppComponent:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';

import { AppComponent } from './app.component';
import { ModalModule } from './modal/modal.module';

@NgModule({
  imports:      [
                  BrowserModule,
                  FormsModule,
                  // Importação do módulo
                  // do modal:
                  ModalModule,
                ],
  declarations: [ AppComponent ],
  bootstrap:    [ AppComponent ]
})
export class AppModule { }
app.module.ts

Agora com o ModalModule importado, podemos testar o modal adicionando algumas linhas de código no app.component.html:

<button (click)="modal.toggle()">
  Mostrar modal
</button>

<app-modal #modal>
  <--
      O conteúdo dentro do <app-modal> 
      será recebido no <ng-content> que
      está no modal.componente.html
  -->
  <h1>Lorem, ipsum dolor.</h1>
  <p>
    Lorem ipsum, dolor sit amet
    consectetur adipisicing elit.
    Earum dicta non alias distinctio,
    aliquid aperiam ipsum provident
    quae nihil voluptatibus veniam
    fuga similique facilis consequatur
    reiciendis recusandae. Velit magni
    distinctio corporis eaque ad itaque
    ex impedit accusamus. Debitis pariatur
    cum quam rerum reprehenderit quod
    iure, in explicabo facere vero nemo?
  </p>
  
  <!--
       "acaoPrimaria()" é um método
       dentro do app.component.ts
  -->
  <button
    (click)="acaoPrimaria(); modal.toggle();"
    class="btn-primario">Ação</button>

  <button
    class="btn-secundario">Ação 2</button>
</app-modal> 
app.component.html

O #modal serve para pegarmos a referência do componente. Desta forma conseguimos dentro do próprio template chamar os métodos do modal.


Não precisa ser necessariamente #modal , poderia ser #abc ou qualquer outro nome. Veja mais detalhes na documentação oficial: https://angular.io/guide/template-reference-variables


O resultado é o modal em funcionamento:

Modal funcionando

Inclusão do animations

Agora que já temos um componente de modal, vamos evoluir um pouco para adicionar uma transição visual mais agradável. Vamos adicionar um efeito fade no fundo escuro que fica por trás (overlay) e adicionar uma animação do tipo slide na parte branca do modal.

Para executar esta tarefa vamos recorrer ao animations do Angular. O primeiro passo é importar o BrowserAnimationsModule. A importação deve ser feita no módulo raiz da sua aplicação. Neste exemplo o módulo root é o AppModule:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { FormsModule } from '@angular/forms';

import { AppComponent } from './app.component';
import { ModalModule } from './modal/modal.module';

@NgModule({
  imports:      [
                  BrowserModule,
                  BrowserAnimationsModule,
                  FormsModule,
                  ModalModule,
                  
                ],
  declarations: [ AppComponent ],
  bootstrap:    [ AppComponent ]
})
export class AppModule { }
app.module.ts

Agora vamos configurar os efeitos no componente modal e na sequência vêm a  explicação:

import { Component } from '@angular/core';
import {
  trigger,
  style,
  animate,
  transition } from '@angular/animations';

@Component({
  selector: 'app-modal',
  templateUrl: 'modal.component.html',
  styleUrls: ['modal.component.css'],
  animations: [
    // fundo escuro que fica atrás do modal
    trigger('overlay', [
      transition(':enter', [
        // Inicia com o opacity zerado
        style({ opacity: 0 }),
        
        // efetua a animação de 250ms para o
        // o opacity de 0 até 0.5
        animate('250ms', style({ opacity: .5 })),
      ]),
      transition(':leave', [
        // Quando for esconder o overlay, 
        // anima do opacity atual, 0.5, até
        // o valor 0
        animate('500ms', style({ opacity: 0 }))
      ])
    ]),
    
    // animação na parte branca do modal
    trigger('modal', [
      transition(':enter', [
        // inicia com o modal "lá em cima"
        style({ top: -999 }),
        
        // e finaliza com o modal no meio da tela
        animate('500ms', style({ top: '50%' })),
      ]),
      transition(':leave', [
      
        // para esconder o modal, basta
        // "jogar ele lá para cima da tela"
        animate('250ms', style({ top: -999 }))
      ])
    ]),
  ]
})
export class ModalComponent {
  mostrar: boolean = false;

  toggle () {
    this.mostrar = !this.mostrar;
  }
}
modal.component.ts
<ng-container *ngIf="mostrar">
  <div class="modal" @modal>
    <ng-content></ng-content>
  </div>
  <div (click)="toggle()"
       @overlay
       class="overlay"></div>
</ng-container>
modal.component.html

Foram criadas duas trigger, uma para cada elemento (overlay e o corpo do modal). Cada trigger acima tem duas possíveis transições:

  • :enter -> ocorre quando entramos no *ngIf, ou seja, o modal será mostrado na tela
  • :leave -> ocorre quando não entramos no *ngIf, ou seja, o modal está sendo escondido

Para vincular a  trigger ao elemento HTML utilizamos o prefixo @ seguido do nome da trigger, conforme está no modal.component.html acima.

Veja o resultado logo abaixo:

Modal com animação

Corrigindo a altura do modal

Na forma que nosso componente está, se adicionarmos muito conteúdo dentro do modal ele vai passar da área visível da tela. Para resolver este problema vamos ajustar a classe .modal dentro do modal.component.css:

.modal {
  /* ... (ocultado) ... */
  min-width: 320px;
  max-height: 90vmax;
  overflow-y: scroll;
}

/* ... (ocultado) ... */
modal.component.css

Adicionamos uma largura mínima de 320px, uma altura máxima de 90% da altura do viewport e caso o conteúdo exceda esse espaço vertical, uma barra de scroll será apresentada.

Modal com o ajuste na altura

Considerações

O modelo do componente de modal proposto neste texto possui um mecanismo bem simples. Caso você necessite controlar várias instâncias ao mesmo tempo será necessário evoluir um pouco mais esta ideia de forma análoga ao que foi mostrado no post Como construir um componente para notificações.

Um outro ponto que poderíamos evoluir, é que o modal só é exibido quando chamamos o método toggle(), mas poderíamos criar um @Input() exibir para permitir uma outra forma de controle na exibição do modal: <app-modal [exibir]="true">...

Também não chegamos a adicionar um botão X para fechar o modal nem falamos sobre acessibilidade. Em um próximo texto abordarei estes assuntos.

Modal - Angular - StackBlitz
Como construir um componente de modal com efeitos de transição utilizando o animations do Angular
Exemplo da implementação