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);
}
}
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>
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();
}
}
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
});
}
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>
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)
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.
Isto por causa do tipo
ComponentType
que está em@angular/cdk/overlay
.É possível substituir o
ComponentType
peloType
que está em@angular/core
.Então, eu fiz as seguintes substituições:
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étodoresolveComponentFactory
. Assim a gente não precisa mais fazer o cast paraany
. Ou seja: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.Show de bola Wellington, que legal que gostou e agregou mais ainda aqui, agradeço demais.
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:
Tem alguma ideia do que poderia ser?