Build Angular Modal Without Libs - Angular Dynamic Component
In this post you will learn how to implement a modal inside Angular without any additional libraries.
Just from the beginning I want to tell you that if you just need a library with a modal you can look an ng-bootstrap or angular-material.
For example ng-bootstrap has a really nice and reusable modal that I used in a lot of places. Both are perfectly fine but if you want to build something yourself (which you can understand completely and can support better) then let's build it from scratch.
The main problem with modals is that we want to render them in body to avoid overflow and z-index problems.
This is why all reusable modals are always rendered in the body.
Initial project
<button>Open modal</button>
Inside app component I have just a button which should open our modal.
// src/app/modal/components/modal/modal.component.html
<div class="modal">
<div class="modal-header">
HERE IS TITLE
<span class="modal-close">✕</span>
</div>
<div class="modal-content">
<ng-content></ng-content>
</div>
<div class="modal-footer">
<button>Submit</button>
</div>
</div>
<div class="modal-backdrop"></div>
Here is markup of our modal without any logic. This is plain HTML. The only thing here is ng-content
which we will use later to render custom template inside.
// src/app/modal/components/modal/modal.component.css
.modal-backdrop {
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.4);
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
z-index: 2;
display: flex;
justify-content: center;
align-items: center;
}
.modal {
z-index: 3;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 500px;
height: auto;
border: olive 1px solid;
background-color: white;
border-radius: 5px;
padding: 20px;
}
.modal-header {
display: flex;
justify-content: space-between;
font-size: 18px;
margin-bottom: 20px;
}
.modal-content {
margin-bottom: 20px;
}
.modal-footer {
display: flex;
justify-content: flex-end;
}
.modal-close {
cursor: pointer;
}
Here are prepared CSS for our form so we fully focus on Angular.
Thinking about architecture
Now we need to think how we want to use our modal. We want to be able to open it from any place and in order to do that we need to use a service. An additional problem is that we want to pass inside a custom template that we want to render as a content.
First let's create a function which opens our modal.
<button (click)="openModal(modalTemplate)">Open modal</button>
<ng-template #modalTemplate>
<div>This is our custom modal content</div>
</ng-template>
Here we defined a custom template that we pass in openModal
function as an argument.
export class AppComponent {
constructor(private modalService: ModalService) {}
openModal(modalTemplate: TemplateRef<any>) {
this.modalService
.open(modalTemplate, { size: 'lg', title: 'Foo' })
.subscribe((action) => {
console.log('modalAction', action);
});
}
}
Here is how our openModal
function must look like. We inject modalService
to open a modal from any place. We pass our custom template in open
method and provide additional parameters as a second. Open
method returns for us an observable so we can add subscribe
we wait when the modal is submitted. This subscribe is our callback.
Modal service
Now we need to create modalService
and open
method inside.
export class ModalService {
constructor(
private resolver: ComponentFactoryResolver,
private injector: Injector,
@Inject(DOCUMENT) private document: Document
) {}
open(content: TemplateRef<any>, options?: { size?: string; title?: string }) {
const modalComponentFactory = this.resolver.resolveComponentFactory(
ModalComponent
);
const contentViewRef = content.createEmbeddedView(null);
const modalComponent = modalComponentFactory.create(this.injector, [
contentViewRef.rootNodes,
]);
modalComponent.hostView.detectChanges();
this.document.body.appendChild(modalComponent.location.nativeElement);
}
}
Here we created our ModalService
and open
function. Inside we create modalComponentFactory
which knows how to create ModalComponent
. Then we created contentViewRef
from content
that we passed as an argument which will be rendered in ng-content
. Then we create modalComponent
with this content and called detectChanges
. The last line is to append our DOM element to body.
But it is not all. We must pass all events to our ModalComponent
.
modalComponent.instance.size = options?.size;
modalComponent.instance.title = options?.title;
modalComponent.instance.closeEvent.subscribe(() => this.closeModal());
modalComponent.instance.submitEvent.subscribe(() => this.submitModal());
modalComponent.hostView.detectChanges();
Before detectChanges
we passed size
and title
to our component. We also subscribed to closeEvent
and submitEvent
which are outputs inside our modal. We will create this.closeModal
and this.submitModal
later.
export class ModalService {
private modalNotifier?: Subject<string>;
open(...) {
...
modalComponent.hostView.detectChanges();
this.document.body.appendChild(modalComponent.location.nativeElement);
this.modalNotifier = new Subject();
return this.modalNotifier?.asObservable();
}
closeModal() {
this.modalNotifier?.complete();
}
submitModal() {
this.modalNotifier?.next('confirm');
this.closeModal();
}
}
Here we created a Subject
at the end of open
function and returned it as observable. It allows us to subscribe outside and wait for the submit. We also added closeModal
where we just complete our subscription and submitModal
where we trigger our Subject and complete it.
Modal component
Now we need to implement our Modal. It is relatively easy after our service.
export class ModalComponent {
@Input() size? = 'md';
@Input() title? = 'Modal title';
@Output() closeEvent = new EventEmitter();
@Output() submitEvent = new EventEmitter();
constructor(private elementRef: ElementRef) {}
close(): void {
this.elementRef.nativeElement.remove();
this.closeEvent.emit();
}
submit(): void {
this.elementRef.nativeElement.remove();
this.submitEvent.emit();
}
}
Here we implemented Input
and Output
that we set in modalService. We also make correct emits and remove the element from the DOM.
<div class="modal {{ size }}">
<div class="modal-header">
{{ title }}
<span class="modal-close" (click)="close()">✕</span>
</div>
<div class="modal-content">
<ng-content></ng-content>
</div>
<div class="modal-footer">
<button (click)="submit()">Submit</button>
</div>
</div>
<div class="modal-backdrop" (click)="close()"></div>
This is our finished markup where we binded close
, submit
and rendered inputs correctly.
As you can see our modal works just fine, we can open it from any place and all our inputs and outputs are working perfectly.
Want to conquer your next JavaScript interview? Download my FREE PDF - Pass Your JS Interview with Confidence and start preparing for success today!
📚 Source code of what we've done