DEV Community

Cover image for Criando um modal com conteúdo dinâmico - Angular
Iury Nogueira
Iury Nogueira

Posted on • Edited on

Criando um modal com conteúdo dinâmico - Angular

Olá eu sou o Goku, meu primeiro post aqui 🎉, talvez você já tenha passado por uma situação de criar um componente com conteúdo de outro, existem algumas maneiras de criar um componente de forma dinâmica, para exemplificar esse comportamento vou utilizar como exemplo a implementação de um modal que tem seu conteúdo (corpo) modificado através de outro componente, vamos para a prática:

Vamos começar implementando nosso componente de modal, onde teremos os botões de concluir, cancelar, título e corpo (dinâmico).

import {
  Component,
  ComponentFactoryResolver,
  EventEmitter,
  Input,
  OnDestroy,
  OnInit,
  Output,
  ViewChild,
  ViewContainerRef,
} from '@angular/core';
import { ComponentType } from '@angular/cdk/overlay';

@Component({
  selector: 'dynamic-modal',
  templateUrl: 'dynamic-modal.component.html',
  styleUrls: ['dynamic-modal.component.less'],
})
export class DynamicModalComponent implements OnInit, OnDestroy {
  constructor(private resolverFactory: ComponentFactoryResolver) {
  }

  @Input() title: string = '';
  @Input() body!: ComponentType<{}>;
  @Output() closeMeEvent = new EventEmitter();
  @Output() confirmEvent = new EventEmitter();

  @ViewChild('viewContainer', {read: ViewContainerRef, static: false}) viewContainer!: ViewContainerRef;

  ngOnInit(): void {
    console.log('Modal init');
  }

  closeMe() {
    this.closeMeEvent.emit();
  }
  confirm() {
    this.confirmEvent.emit();
  }

  ngOnDestroy(): void {
    console.log('Modal destroyed');
  }

  ngAfterViewInit() {
    const factory = this.resolverFactory.resolveComponentFactory(this.body as any);
    this.viewContainer.createComponent(factory);
  }
}
Enter fullscreen mode Exit fullscreen mode

Nosso body será o componente informado via service que será renderizado por nossa factory como está implementado no ngAfterViewInit.

Adicione também o HTML do componente de modal. Você sabia que os componentes do angular são desse tipo? ComponentType.

<div
  style="
    width: 500px;
    height: auto;
    border: 1px solid black;
    background-color: white;
    border-radius: 15px;
    display: flex;
    justify-content: center;
    align-items: center;
    flex-direction: column;
  "
>
  <h1 class="title">{{ title }}</h1>
  <div #viewContainer></div>
  <div>
    <button (click)="closeMe()">Fechar</button>
    <button (click)="confirm()">Salvar</button>
  </div>
</div>
Enter fullscreen mode Exit fullscreen mode

a div que contém #viewContainer será responsável por renderizar nosso conteúdo dinâmico. Para chamar o modal que acabamos de criar vamos precisar adicionar um serviço que será responsável por receber os parâmetros necessários para a construção do modal como título e conteúdo (body dinâmico). Segue abaixo a implementação do service.

import {
  ComponentFactoryResolver,
  ComponentRef,
  Injectable,
  ViewContainerRef,
} from '@angular/core';
import { Subject } from 'rxjs';
import { DynamicModalComponent } from './dynamic-modal.component';
import { ComponentType } from '@angular/cdk/overlay'; 

@Injectable({ providedIn: 'root' })
export class ModalService {
  private componentRef!: ComponentRef<DynamicModalComponent>;
  private componentSubscriber!: Subject<string>;
  constructor(private resolver: ComponentFactoryResolver) {}

  openModal(entry: ViewContainerRef, modalTitle: string, modalBody: ComponentType<{}>) {
    let factory = this.resolver.resolveComponentFactory(DynamicModalComponent);
    this.componentRef = entry.createComponent(factory);
    this.componentRef.instance.title = modalTitle;
    this.componentRef.instance.body = modalBody;
    this.componentRef.instance.closeMeEvent.subscribe(() => this.closeModal());
    this.componentRef.instance.confirmEvent.subscribe(() => this.confirm());
    this.componentSubscriber = new Subject<string>();
    return this.componentSubscriber.asObservable();
  }

  closeModal() {
    this.componentSubscriber.complete();
    this.componentRef.destroy();
  }

  confirm() {
    this.componentSubscriber.next('confirm');
    this.closeModal();
  }
}

Enter fullscreen mode Exit fullscreen mode

esse serviço é responsável por informar os @Inputs do nosso DynamicModalComponent, note que informamos o conteúdo do modal na seguinte linha this.componentRef.instance.body = modalBody;, agora nós temos um serviço que cria nosso modal (DynamicModalComponent) com título e conteúdo dinâmicos, com isso só precisamos agora chamar nosso serviço e informar os conteúdos para ele, essa é a parte que nós vamos chamar no dia a dia para criar um modal. Na tela que você precisa chamar o modal adicione os seguintes códigos:

constructor(private modalService: ModalService) {}

@ViewChild('modal', { read: ViewContainerRef, static: true })
  entry!: ViewContainerRef;
  sub!: Subscription;

openModal() {
// MyComponent é o componente que será renderizado dentro do seu body
    this.sub = this.modalService
      .openModal(this.entry, 'Título do modal', MyComponent)
      .subscribe((v) => {
        // dispara quando é aberto o modal
      });
  }
Enter fullscreen mode Exit fullscreen mode

no HTML precisamos adicionar o botão obviamente para chamar a função openModal e uma tag para nosso ViewChild localizar.

<button
  (click)="openModal()"
  data-testid="button-login"
>
 Abrir Modal
</button>

<div #modal></div>
Enter fullscreen mode Exit fullscreen mode

e prontinho! Aconselho fortemente criar um module separado para adicionar seus modais contents e esse componente de modal com o serviço dentro do mesmo módulo. Crie também um modelo bacana de modal não use esse layout maravilhoso do post para o projeto kkk e defina como padrão para todo o seu sistema, caso um dia o modal venha a ser alterado você precisará alterar somente em um local (modal.component.html).

É isso pessoal, espero que eu tenha conseguido contribuir com o desenvolvimento de vocês, também tenho que aprender e me empenhar para escrever mais por aqui então qualquer feedback
vai ser muito construtivo, obrigado! 🍻

Top comments (3)

Collapse
 
wldomiciano profile image
Wellington Domiciano

Obrigado por este conteúdo, foi muito legal seguir. Testei usando Angular 12 e funcionou perfeitamente seguindo seus passos.

O ponto negativo é que tive que adicionar uma biblioteca extra usando o comando abaixo.

ng add @angular/cdk
Enter fullscreen mode Exit fullscreen mode

Isto por causa do tipo ComponentType que está em @angular/cdk/overlay.


É possível substituir o ComponentType pelo Type que está em @angular/core.

Então, eu fiz as seguintes substituições:

// Troquei isto:
openModal(entry: ViewContainerRef, modalTitle: string, modalBody: ComponentType<{}>) {

// Por isto:
openModal(entry: ViewContainerRef, modalTitle: string, modalBody: Type<unknown>) {

// E isto:
@Input() body!: ComponentType<{}>;

// Por isto:
@Input() body!: Type<unknown>;
Enter fullscreen mode Exit fullscreen mode

A vantagem de fazer estas substituições, além de não precisar depender do @angular/cdk que é uma biblioteca separada, é que o tipo fica certinho com o que é esperado pelo método resolveComponentFactory. Assim a gente não precisa mais fazer o cast para any. Ou seja:

// Isto:
const factory = this.resolverFactory.resolveComponentFactory(this.body as any);

// Vira isto (sem cast):
const factory = this.resolverFactory.resolveComponentFactory(this.body);
Enter fullscreen mode Exit fullscreen mode

Outra mudança que eu fiz foi trocar <div #modal></div> por <ng-container #modal></ng-container>. Eu achei melhor fazer assim porque como a div só serve meio que para marcar o local que a modal será adicionada, é desnecessário que ela apareça e esteja presente no template e como o ng-container é um element invisivel, fez mais sentido para mim usá-lo.

Collapse
 
iurynogueira profile image
Iury Nogueira

Show de bola Wellington, que legal que gostou e agregou mais ainda aqui, agradeço demais.

Collapse
 
camilasantos05 profile image
camilasantos05 • Edited

Criei um componente semelhante, porém, o meu body é uma string, onde coloco esse texto dentro de um ngxSummernote no meu html.
Tudo certo até aí, porém, quando eu chamo a modal no meu component e ela chama o service para abrir a modal, ele cai no afterViewInit e na hora do "createComponent", ele me retorna o seguinte erro:

QuestionarioComponent.html:313 ERROR TypeError: Cannot read properties of undefined (reading 'createComponent')
    at ModalTextFormatService.openModal (modal-text-format.service.ts:19:35)
    at QuestionarioComponent.openModalDiligencia (questionario.component.ts:2356:12)
    at QuestionarioComponent.diligencia (questionario.component.ts:417:12)
    at Object.eval [as handleEvent] (QuestionarioComponent.html:313:41)
    at handleEvent (core.js:34777:77)
    at callWithDebugContext (core.js:36395:1)
    at Object.debugHandleEvent [as handleEvent] (core.js:36031:1)
    at dispatchEvent (core.js:22519:1)
    at core.js:33709:1
    at HTMLButtonElement.<anonymous> (platform-browser.js:1789:1)QuestionarioComponent.html:313 ERROR TypeError: Cannot read properties of undefined (reading 'createComponent')
    at ModalTextFormatService.openModal (modal-text-format.service.ts:19:35)
    at QuestionarioComponent.openModalDiligencia (questionario.component.ts:2356:12)
    at QuestionarioComponent.diligencia (questionario.component.ts:417:12)
    at Object.eval [as handleEvent] (QuestionarioComponent.html:313:41)
    at handleEvent (core.js:34777:77)
    at callWithDebugContext (core.js:36395:1)
    at Object.debugHandleEvent [as handleEvent] (core.js:36031:1)
    at dispatchEvent (core.js:22519:1)
    at core.js:33709:1
    at HTMLButtonElement.<anonymous> (platform-browser.js:1789:1)QuestionarioComponent.html:313 ERROR TypeError: Cannot read properties of undefined (reading 'createComponent')
    at ModalTextFormatService.openModal (modal-text-format.service.ts:19:35)
    at QuestionarioComponent.openModalDiligencia (questionario.component.ts:2356:12)
    at QuestionarioComponent.diligencia (questionario.component.ts:417:12)
    at Object.eval [as handleEvent] (QuestionarioComponent.html:313:41)
    at handleEvent (core.js:34777:77)
    at callWithDebugContext (core.js:36395:1)
    at Object.debugHandleEvent [as handleEvent] (core.js:36031:1)
    at dispatchEvent (core.js:22519:1)
    at core.js:33709:1
    at HTMLButtonElement.<anonymous> (platform-browser.js:1789:1)QuestionarioComponent.html:313 ERROR TypeError: Cannot read properties of undefined (reading 'createComponent')
    at ModalTextFormatService.openModal (modal-text-format.service.ts:19:35)
    at QuestionarioComponent.openModalDiligencia (questionario.component.ts:2356:12)
    at QuestionarioComponent.diligencia (questionario.component.ts:417:12)
    at Object.eval [as handleEvent] (QuestionarioComponent.html:313:41)
    at handleEvent (core.js:34777:77)
    at callWithDebugContext (core.js:36395:1)
    at Object.debugHandleEvent [as handleEvent] (core.js:36031:1)
    at dispatchEvent (core.js:22519:1)
    at core.js:33709:1
    at HTMLButtonElement.<anonymous> (platform-browser.js:1789:1)QuestionarioComponent.html:313 ERROR TypeError: Cannot read properties of undefined (reading 'createComponent')
    at ModalTextFormatService.openModal (modal-text-format.service.ts:19:35)
    at QuestionarioComponent.openModalDiligencia (questionario.component.ts:2356:12)
    at QuestionarioComponent.diligencia (questionario.component.ts:417:12)
    at Object.eval [as handleEvent] (QuestionarioComponent.html:313:41)
    at handleEvent (core.js:34777:77)
    at callWithDebugContext (core.js:36395:1)
    at Object.debugHandleEvent [as handleEvent] (core.js:36031:1)
    at dispatchEvent (core.js:22519:1)
    at core.js:33709:1
    at HTMLButtonElement.<anonymous> (platform-browser.js:1789:1)QuestionarioComponent.html:313 ERROR TypeError: Cannot read properties of undefined (reading 'createComponent')
    at ModalTextFormatService.openModal (modal-text-format.service.ts:19:35)
    at QuestionarioComponent.openModalDiligencia (questionario.component.ts:2356:12)
    at QuestionarioComponent.diligencia (questionario.component.ts:417:12)
    at Object.eval [as handleEvent] (QuestionarioComponent.html:313:41)
    at handleEvent (core.js:34777:77)
    at callWithDebugContext (core.js:36395:1)
    at Object.debugHandleEvent [as handleEvent] (core.js:36031:1)
    at dispatchEvent (core.js:22519:1)
    at core.js:33709:1
    at HTMLButtonElement.<anonymous> (platform-browser.js:1789:1)QuestionarioComponent.html:313 ERROR TypeError: Cannot read properties of undefined (reading 'createComponent')
    at ModalTextFormatService.openModal (modal-text-format.service.ts:19:35)
    at QuestionarioComponent.openModalDiligencia (questionario.component.ts:2356:12)
    at QuestionarioComponent.diligencia (questionario.component.ts:417:12)
    at Object.eval [as handleEvent] (QuestionarioComponent.html:313:41)
    at handleEvent (core.js:34777:77)
    at callWithDebugContext (core.js:36395:1)
    at Object.debugHandleEvent [as handleEvent] (core.js:36031:1)
    at dispatchEvent (core.js:22519:1)
    at core.js:33709:1
    at HTMLButtonElement.<anonymous> (platform-browser.js:1789:1)
Enter fullscreen mode Exit fullscreen mode

Tem alguma ideia do que poderia ser?