I) Introduction
Modal is simply an interactive window that hides the principal page to provide the user options according to his action.
For example, given a list of items with a delete button for each row, when the user clicks the delete button, a modal appears requiring the user to either confirm his choice (delete item) or close the modal. As you may notice, modal interaction is a great choice since it offers a nice user experience.
In this quick tutorial, we will try to build a custom reusable modal with angular, which makes it easy to maintain and reuse.
II) Modal elements
To build this modal we need 3 main elements
Modal service which is responsible for creating/destroying modal.
Modal component containing modal information ( body, title, buttons ) and, it sends events to modalService ( confirm/close ).
Hosting component contains a reference to where the modal is going to appear and listen to modalService events. It does not know anything about the modal.
NB: At the end of each section, you find the code associated with.
1)Modal service
Our service will hold 3 methods:
openModal : which is responsible for creating modal and add it to a specific component (hosting component).
closeModal : which is responsible for destroying modal, after the user clicks on the close button.
confirm : which is the event triggered when the user clicks on confirm button, it emits "confirm" to hosting component.
Our service will hold 2 private attributes:
componentRef : which keeps track of the modal created.
componentSubscriber : which is a subject returned after the modal created. it is responsible for sending events to the component holding the modal.
let's dig deep down in our methods:
openModal
openModal method will receive 3 parameters:
entry: which is the modal container (where the modal is going to appear) of type viewContainerRef.
Angular doc:
viewContainerRef represents a container where one or more views can be attached to a component.
modalTitle: which is the modal title.
modalBody: representing modal body.
The question here is how to create a modal component, and add it to the modal container ? 😳
entry ( viewContainerRef ) has a magic method called, createComponent. This method instantiate a component and add it to the viewContainerRef (entry). It takes a component factory and returns component instance, which gives us access to component instance and related objects.
But how to create a component factory from a component? 😕
Angular provides us a componentFactoryResolver class that takes a component and returns a componentFactory.
Angular doc:
componentFactoryResolver is a simple registry that maps Components to generated ComponentFactory classes that can be used to create instances of components.
ComponentFactory is the base class for a factory that can create a component dynamically. Instantiate a factory for a given type of component with resolveComponentFactory().
Great !! 😃
We injected this class ( ComponentFactoryResolver ) in service constructor, and we created a componentFactory ( factory ) in openModal method.
After modal instance created, we are able then to provide the modal by title input, body input, and subscribe to different output events ( closeMeEvent , confirmEvent ) to handle them in user interaction.
Then, in this method we return a subject as observable, this observable is useful to the hosting component to be notified of user interaction with the modal ( "confirm" ) to do specific logic.
closeModal :
This method is simple, it is responsible for destroying the modal and complete the subscriber. Hosting component is not concerned by this event.
confirm :
which is responsible for sending to the hosting component "confirm" and then close the modal
export class ModalService {
private componentRef!: ComponentRef<ModalComponent>;
private componentSubscriber!: Subject<string>;
constructor(private resolver: ComponentFactoryResolver) {}
openModal(entry: ViewContainerRef, modalTitle: string, modalBody: string) {
let factory = this.resolver.resolveComponentFactory(ModalComponent);
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();
}
}
2)Modal component
Our modal component is a simple angular component containing :
title : as @input()
body : as @input()
closeMeEvent: as @Output()
confirmEvent: as @Output()
closeMe : emitting closeMeEvent to modalService
confirm : emitting confirmEvent to modalService
export class ModalComponent implements OnInit, OnDestroy {
constructor() {}
@Input() title: string = '';
@Input() body: string = '';
@Output() closeMeEvent = new EventEmitter();
@Output() confirmEvent = new EventEmitter();
ngOnInit(): void {
console.log('Modal init');
}
closeMe() {
this.closeMeEvent.emit();
}
confirm() {
this.confirmEvent.emit();
}
ngOnDestroy(): void {
console.log(' Modal destroyed');
}
}
3)Hosting component
Hosting component, is the component where the modal is going to be created. Home component in our example.
This component must have a HTML reference element representing the modal container, and the event triggering modal creation ( createModal ).
The host component is responsible for providing modal service by viewContainerRef (entry). To get this view we use @viewChild decorator with the reference element specified in the view(#modal).
Angular doc:
@viewChild is a property decorator that configures a view query. The change detector looks for the first element or the directive matching the selector in the view DOM
This component is responsible also for subscribing to modalService openModal to listen for events streams. If it receives "confirm", our logic will be executed.
That's it ! your modal is working fine. 💪
export class HomeComponent implements OnInit, OnDestroy {
constructor(private modalService: ModalService) {}
@ViewChild('modal', { read: ViewContainerRef })
entry!: ViewContainerRef;
sub!: Subscription;
ngOnInit(): void {}
createModal() {
this.sub = this.modalService
.openModal(this.entry, 'Are you sure ?', 'click confirm or close')
.subscribe((v) => {
//your logic
});
}
ngOnDestroy(): void {
if (this.sub) this.sub.unsubscribe();
}
}
<button (click)="createModal()">Delete me</button>
<div #modal></div>
III) Conclusion
In this tutorial, we proposed a general pattern of how to implement a modal in angular, which consists of three elements:modal component, modal service, and hosting component.
Hope it is clear and helpful. Comment below for any suggestions, clarification or issues.
Waiting for your feedback concerning this pattern ✌️.
You find full code in my github repo Angular reusable modal pattern
Top comments (2)
Great post! I'm sure you know this but for those reading this that don't know, you can also use
@angular/cdk
(the Angular Component Dev Kit) and use their Overlay service to handle a lot of this stuff for you, including some incredibly powerful positioning and scroll behavior APIs that you won't have to implement yourself. (such as if you wanted to attach an overlay to an element rather than positioning it globally)How to create modal from injected service if this service is initialized in AfterInit of main app component? There are still no any ViewChild available!