As I was starting a new web application project I came across a need for a simple modal. Say what you will about popups or modals they serve a purpose in current web apps. Since my project was all green field, I had the choice of the latest and greatest libraries. I could reach for a simple Bootstrap modal, style it, and call it a day. But I didn’t want another library for just one thing, especially something simple. And I quickly started to see how much I would need to override in order to get an off the shelf solution to work with applications style.
So I created a simple modal service, with a little help from my internet friends, thanks Ben Nadel, that would be low overhead and provide me with the flexibility my application needed for the long term.
Problem:
Off the shelf modals = unnecessary and not easily extended; bloated libraries = BAD!
Solution:
Create a simple modal service that can be used throughout the application for things like communicating error messages, confirmation dialogs, user forms, etc.
The Process
To start we need a simple modal component that will contain the global modal styles and functionality. The modal component can then display specific information within the
modal.component.ts
import { Component, HostListener, Input, OnInit } from '@angular/core';
import { ModalService } from './modal.service';
/**
* ModalComponent - This class represents the modal component.
* @requires Component
*/
@Component({
selector: 'app-modal',
styleUrls: ['app/modal.scss'],
template: '
<div class="modal-container" *ngIf="isOpen">
<div class="modal-overlay" (click)="close(true)"></div>
<div class="app-modal">
<div class="title">
<h3 *ngIf="modalTitle" [innerHTML]="modalTitle"></h3>
<button *ngIf="!blocking && closebtn"
class="btn-close" (click)="close()">X</button>
</div>
<div class="body">
<ng-content></ng-content>
</div>
</div>
</div>
'
})
export class ModalComponent implements OnInit {
isOpen: boolean = false;
@Input() closebtn: boolean;
@Input() modalId: string;
@Input() modalTitle: string;
@Input() blocking: boolean;
@HostListener('document:keyup', ['$event'])
/**
* keyup - Checks keys entered for the 'esc' key, attached to hostlistener
*/
keyup(event: KeyboardEvent): void {
if (event.keyCode === 27) {
this.modalService.close(this.modalId, true);
}
}
constructor(private modalService: ModalService) { }
/**
* ngOnInit - Initiated when component loads
*/
ngOnInit() {
this.modalService.registerModal(this);
}
/**
* close - Closes the selected modal
*/
close(checkBlocking = false): void {
this.modalService.close(this.modalId, checkBlocking);
}
}
_modal.scss
Give the modal some basic styles. This will position the modal in the middle of the screen, slightly opaque background. Anything passed into the
.modal-container {
.modal-overlay {
background: rgba(darkgray, 0.65);
bottom: 0;
left: 0;
position: fixed;
right: 0;
top: 0;
z-index: 1100;
}
.app-modal {
background: white;
border-radius: 3px;
left: calc(50% - 250px);
padding: 0;
max-height: calc(100% - 100px);
overflow-y: auto;
position: fixed;
top: 50px;
width: 500px;
z-index: 1101;
.title {
height: 30px;
h3 {
display: inline-block;
margin-top: 0 !important; //to overwrite bootstrap style for h3
}
.btn-close {
background: lightgray;
border: 0;
border-radius: 50%;
float: right;
height: 25px;
width: 25px;
}
}
}
}
modal.service.ts
import { Injectable } from '@angular/core';
import { ModalComponent } from './modal.component';
/**
* ModalService - Service used to interact with the Modal Component
*/
@Injectable()
export class ModalService {
private modals: Array<ModalComponent>;
constructor() {
this.modals = [];
}
/**
* close - Closes the selected modal by searching for the component and setting
* isOpen to false
* Note: If a modal is set to be 'blocking' a user click outside of the modal will
* not dismiss the modal, this is off my default
* @param { String } modalId The id of the modal to close
*/
close(modalId: string, checkBlocking = false): void {
let modal = this.findModal(modalId);
if (modal) {
if (checkBlocking && modal.blocking) {
return;
}
setTimeout(() => {
modal.isOpen = false;
}, 250);
}
}
/**
* findModal - Locates the specified modal in the modals array
* @param { String } modalId The id of the modal to find
*/
findModal(modalId: string): ModalComponent {
for (let modal of this.modals) {
if (modal.modalId === modalId) {
return modal;
}
}
return null;
}
/**
* open - Opens the specified modal based on the suplied modal id
* @param { String } modalId The id of the modal to open
*/
open(modalId: string): void {
let modal = this.findModal(modalId);
if (modal) {
setTimeout(() => {
modal.isOpen = true;
}, 250);
}
}
/**
* registerModal - Registers all modal components being used on initialization
* @param { Object } newModal The new modal to add to the array of available modals
*/
registerModal(newModal: ModalComponent): void {
let modal = this.findModal(newModal.modalId);
// Delete existing to replace the modal
if (modal) {
this.modals.splice(this.modals.indexOf(modal), 1);
}
this.modals.push(newModal);
}
}
Using the new modal component is easy. We call it within our application:
<app-modal modalId="errorModal" blocking="true" closebtn="true">
<p>An Error occured, please try this action again.</p>
...
</app-modal>
And then we can reference that modal from another component.
...
this.modalService.openModal('errorModal');
...