Managing Multiple Bootstrap Modals

Introduction

If you have managed to find your way to this blog, there is no doubt that you also have some experience with bootstrap. We here at Arroyo Labs love the bootstrap as it provides us the tools to enhance the user experience of our products. Although we love its shiny new toy aspects of bootstrap,they always come with some unexpected complexity.

This blog post was inspired by a real case we have ran into with one of our client’s project that requires alternating the two bootstrap modals. The following animation exactly replicates the problem we had to a simplest form:

Modal has background scrolling enabled which causes issues.

Although the snippets of code in this blog are AngularJS 2 and typescript, I have written this post in hopes that it will be helpful to many who are unfamiliar with them. Now lets continue…

The following describes few notes about our scenario:

  1. There are two modals currently present which are creatively named ModalOne and ModalTwo. The view of each modal will alternate at a specified button click. For example, you should be able to call ModalTwo while ModalOne is open and vice versa. The transition between ModalOne’s closing and ModalTwo’s opening should be seamless and without problem.
  2. Each respective button and modal are separated into different Angular Components. They are connected by Angular Services and Observables passing in Boolean values to signify whether to show the modal or not. This allows us to invoke the modal from anywhere in the web application at a click of the button.
  3. The Observables and Services are set up so that the modals would show and hide with the following code:
    /*Following is the HTML template from ModalOneComponent.ts*/
    @Component({
     selector: 'app-modal-one',
     template: `
     
       <div class="modal fade" bsModal #modal1="bs-modal" [config]="   {backdrop: 'static'}"
     tabindex="-1" role="dialog" aria-labelledby="mySmallModalLabel" aria-hidden="true" (onHidden)="hideModal()">
         <div class="modal-dialog modal-sm">
           <div class="modal-content">
             <div class="modal-header">
               <h4 class="modal-title pull-left">Static modal</h4>
               <button type="button" class="close pull-right" aria-label="Close" (click)="closeModal()">
                 <span aria-hidden="true">&times;</span>
               </button>
             </div>
             <div class="modal-body">
               This is ModalOne.
               <br>
     
               <button type="button" class="btn btn-primary" (click)="clickModalTwo($event)">ModalTwo</button>
    
             </div>
            </div>
          </div>
     </div>
     `
    })
    /*Following is typescript code from ModalOneComponent.ts*/
    export class ModaloneComponent {
    
    public showModal$:Subscription;
    private modalStatus:any;
    
    @ViewChild('modal1') public modal1: ModalDirective;
    
    constructor(
    
      private modalOneService:ModalOneService,
      private modalTwoService:ModalTwoService
    
    ) {
    
      this.showModal$=this.modalOneService.showModal$.subscribe(
        showModal$=> {
          this.toggleModal=showModal$;
          this.modalStatus=showModal$;
        }
      ); 
    
    }
    
    //This function is bound to a close button on upper right corner.
    public closeModal() {
      this.modalOneService.showModal=null;
    }
    
     
    //This function is bound to a ModalOne button.
    public clickModalOne():void {
      event.preventDefault();
      this.modalOneService.showModal=true;
    }
    
     
    //This function is bound to a ModalTwo button.
    public clickModalTwo() {
      event.preventDefault();
      this.modalOneService.showModal=false;
    }
    
    public hideModal() {
      if(this.modalStatus!=null) {
        this.modalTwoService.showModal=true;
      }
    
    }
    
    set toggleModal(show:any) {
      if(show===true) {
        this.modal1.show()
      } else {
          this.modal1.hide()
      }
    }
    
    }
/*ModalOneService.ts*/
import { Injectable } from '@angular/core';
import { Subject } from 'rxjs/Subject';

@Injectable()
export class ModalOneService {

  private_showModal$:Subject<any>;
  private_showModal:boolean;

  privatedataStore: {showModal?:boolean};

  constructor(
   ) {
    this._showModal$=newSubject<any>();
   }

   getshowModal$() {
     return this._showModal$.asObservable();
   }

   setshowModal(show:boolean) {

     this._showModal=show;

     this._showModal$.next(this._showModal);

   } 

}

 

If you’ve had extensive experience in bootstrap, you may find that managing multiple bootstrap modals may not be so simple. Unless your goal is to open a modal that is nested inside already open modal, you may see an occurrence as if the two modals are conflicting with each other. We will review more on that topic later in this post.

Deeper into the issue

If you take a closer look inside the developer’s tool in the Chrome browser while opening and closing a single bootstrap modal, “modal-open” class is added to the body element when the modal is open and removes the class when closed.

HTML code when Modal when currently open
HTML code when Modal when currently not open

Inner-workings may be simple when dealing with a single modal but when managing multiple modals, it becomes a lot more problematic.

If you go back to the Chrome developer’s tool again while managing both bootstrap modal, you may realize that the both modal is fighting with eachother to make its respective changes to the body class with class “modal-open”. When ModalOne is to close to make room for ModalTwo, both modals fight to make its respective changes: ModalOne wants to remove the “modal-open” class while ModalTwo tries to add “modal-open” to the body element at the same time.

This is a phenomenon known as Race Condition.

So, what is race condition? This article from techtarget.com explains it the best:

A race condition is an undesirable situation that occurs when a device or system attempts to perform two or more operations at the same time, but because of the nature of the device or system, the operations must be done in the proper sequence to be done correctly.

In this case, before ModalOne has chance to close completely, ModalTwo opens and tries to add modal-open class to the <body> nearly at the same time ModalOne removes the same class. The ModalTwo’s implementation to body tag comes in after and competes/interferes with ModalOne’s change implemented few milliseconds earlier.

The most important part to take away from the definition of race condition is the following:

“…the operation must be done in the proper sequence to be done correctly”.

In this case, ModalOne should be allowed to close itself completely by completing its modal animation fully and make its changes to the <body> before ModalTwo to take action. (and vice versa).

Path to Solution

Although there is no “one-size fits all” solution to this problem as it would depend on the architecture of the app and how the modals are implemented, I hope to at least provide a guideline to help you along the way to a solution.

In the ngx-bootstrap documentation the following directives useful to this case are available. Full documentation is available here: http://valor-software.com/ngx-bootstrap/#/modals#modal-directive

onHidden This event is fired when the modal has finished being hidden from the user (will wait for CSS transitions to complete).
onShown This event is fired when the modal has been made visible to the user (will wait for CSS transitions to complete)

I will only focus on onHidden as it is the only directive we need.

The solution I have created is to implement the onHidden event to the modal:

<!-- ModalOne -->
<div class="modal fade" bsModal 
  #modal1="bs-modal"
  [config]="{backdrop: 'static'}" 
  tabindex="-1"
  role="dialog" 
  aria-labelledby="mySmallModalLabel" 
  aria-hidden="true"
  (onHidden)="hideModal()" >
.....

When the modal hides itself completely, hideModal(), will be called.

The main idea to consider is that after ModalOne is hidden, we have to differentiate in what manner ModalOne is called to be hidden:

Is ModalOne being hidden because we want to close it and go back to the main page? Or is ModalOne being hidden to switch the view to ModalTwo?

In other words, after the ModalOne has been hidden completely, we want the ModalTwo to appear ONLY when ModalTwo button is clicked. If the “x” button in the upper right corner is clicked, the ModalOne should be hidden completely with no subsequent action.

To differentiate such two actions, I have created a property modalStatus, and new methods closeModal and hideModal

/*
  If modalStatus is set true, modal is currently showing
  If modalStatus is set false, modal is currently inactive
  If modalStatus is set null, modal is closed completely.
*/
private modalStatus: any;
//(click) event binds the 'x' button of the Modal to this function
public closeModal() {
  this.modalOneService.showModal = null;
}
//this function is called when modal is completely hidden with the (onHidden) directive.

public hideModal() { 
  if(this.modalStatus!=null) {   
    this.modalTwoService.showModal = true; 
  } 
}

With the solution implemented, the switching of modal is race-condition free.

Managing Modal is now race condition free!

Thank you for making this far! While the example provided has exclusively been AngularJS 2 and above, I hope I have written this post for everyone having similar trouble. Please drop by for any questions and comments.

Thank you!

Update: Added ModalOneService.ts at the introduction section of blog

Next Post

Comments

See how we can help

Lets talk!

Stay up to date on the latest technologies

Join our mailing list, we promise not to spam.