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 element.

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 element can be styled as part of that component to keep code clean and organized.

.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');

...
⤧  Next post Angular 2 Http Service ⤧  Previous post Generate a Unique Tarball