I am the captain of my own starship.

Fly with me, "boldly go where no man has gone before!"

Bootstrap Modal Dialog Interaction with AngularJS 1.6.x

This article is first published in code project on 2/2/2018.

Introduction

It was two years ago, I encountered this issue. I need to open a Bootstrap Modal dialog in an AngularJS application. Somehow, I couldn't get ui-bootstrap to work. ui-boostrap is a 3rd party component that can be integrated with AngularJS to support any Bootstrap related behaviors. It is quite painful to use at times. After some reasearch I realized that without using ui-bootstrap, it is quite easy to add dynamic behaviors to Bootstrap Modal dialogs.

Just a quick summary, to demonstrate the features I am going to cover, three different sample front end applications are bundled in the demo source package. The first one will show the way of open the modal dialog. The second adds one feature on top of the first, it will show how to clear the input field when the modal dialog closes. The last sample will show how to pass the input value to from the caller controller to the modal dialog.

The Basic Architecture

The sample application use Bootstrap 3.3.7 for UI mark up. The main page has a navbar, which serve no functionality at all. Under the navbar, there is a button which can be pushed and show the modal dialog. The application has two controllers, one is for the main application. The other is the controller for the modal dialog. Modal dialog is separated into html file by itself. It is imported into the main page via ngInclude. The first hard part of this is how to trigger the modal dialog to be displayed via AngularJS.

Before we continue to the point of showing the solution to this first issue, let's take a look at the main page. The html code for the main page is in a file called app1.html, and it looks like the following:

<!DOCTYPE html>
<html lang="en">
   <head>
      <meta charset="utf-8">
      <meta http-equiv="X-UA-Compatible" content="IE=edge">
      <meta name="viewport" content="width=device-width, initial-scale=1">
      <title>Angular BootStrap Modal Example 1</title>
      <link href="./assets/bootstrap/css/bootstrap.min.css" rel="stylesheet">
      <link href="./assets/app/css/app.css" rel="stylesheet">
         </head>
   <body>
   <nav class="navbar navbar-inverse navbar-fixed-top">
      <div class="container">
        <div class="navbar-header">
          <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
            <span class="sr-only">Toggle navigation</span>
            <span class="icon-bar"></span>
            <span class="icon-bar"></span>
            <span class="icon-bar"></span>
          </button>
          <a class="navbar-brand" href="#">Angular Bootstrap Modal</a>
        </div>
        <div id="navbar" class="collapse navbar-collapse">
          <ul class="nav navbar-nav">
            <li><a href="#">Home</a></li>
          </ul>
        </div>
      </div>
    </nav>
      <div class="container app-topmargin" ng-app="sampleAngularApp" ng-controller="AppController">
         <div class="row">
            <div class="col-xs-12 col-sm-offset-2 col-sm-8 col-md-offset-3 col-md-3">
               <button class="btn btn-primary" ng-click="openDlg()">Open Modal</button>
            </div>
         </div>
         <ng-include src="'./assets/app/templates/sampleModal.html'"></ng-include>
         
      </div>
      <script src="./assets/jquery/js/jquery-2.2.4.min.js"></script>
      <script src="./assets/bootstrap/js/bootstrap.min.js"></script>
      
      <!-- angular js related libraries -->
      <script src="./assets/angular-1.6.7/angular.min.js"></script>
      <script src="./assets/app/js/sampleModalModule.js"></script>
      <script src="./assets/app/js/app.js"></script>
   </body>
</html>

The most essential part of this file is the following section:

<div class="container app-topmargin" ng-app="sampleAngularApp" ng-controller="AppController">
   <div class="row">
      <div class="col-xs-12 col-sm-offset-2 col-sm-8 col-md-offset-3 col-md-3">
         <button class="btn btn-primary" ng-click="openDlg()">Open Modal</button>
      </div>
   </div>
   <ng-include src="'./assets/app/templates/sampleModal.html'"></ng-include>
         
</div>

The outer div element references an AngularJS app (ng-app) called "sampleAngularApp". It also references a AngularJS controller called "AppController". This basically indicates that any html elements (including the the outer div) are within the control of an AngularJS applicatin called "sampleAngularApp", and its controller "AppController". There is also the button element which has the ng-click event handler. What this does is everything when a user click this button, inside the AngularJS controller, there is a function called openDlg() that would be called to handle this event. As you can guess it, this is the function where the modal dialog would be triggered to display.

Here is the JavaScript code for the main page looks like the following:

var app = angular.module("sampleAngularApp", [ "sampleModalModule" ]);
app.controller("AppController", ["$scope", function ($scope) {
   $scope.openDlg = function () {
      console.log("clicked here...");

      // This is where the magic happens.
      ...
   };
}]);

This is just a typical main AngularJS aplication. The first line defines an AngularJS module called "sampleAngularApp". This is basically what the ng-app references. From the second line onward, it defines the application's controller "AppController". All the controller has is the openDlg() function definition. I put a comment in it to indicate where I put the code to show the modal dialog.

Before I jump into the code regarding how to show the modal dialog. I like to show you how to import the modal dialog. The modal dialog is imported in via the ng-include element. Here is the html code:

<ng-include src="'./assets/app/templates/sampleModal.html'"></ng-include>

What I want to point out is the src attribute. What is enclosed in the double quote is a single quote enclosed string. This is the right syntax. If you remove the single quote enclosure, you will get an error when loading the main page.

The other point I like to make is that the src attribute should point to the template file where it can be found from the current url location. Just keep this in mind. If the template cannot be found, you probbaly will see an error in the developer console.

Here is the html code that defines the modal dialgo template:

<div id="modalDlg" class="modal fade" tabindex="-1" role="dialog" ng-controller="ModalDlgController">
  <div class="modal-dialog" role="document">
    <div class="modal-content">
      <div class="modal-header">
        <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
        <h4 class="modal-title">Sample Modal Dialog</h4>
      </div>
      <div class="modal-body">
         <form>
            <div class="form-group">
               <label for="someValueField">Some Value</label>
               <input type="text" class="form-control" ng-model="someValueField" placeholder="Enter Some Text Value">
            </div>
            <button class="btn btn-default" ng-click="modalButtonClick()">Take Action</button>
         </form>
      </div>
    </div>
  </div>
</div>

This html layout is a little bit different. It is not a full HTML page. You only see the layout of the modal dialog. This is all right, because it will be included into the main page with ng-include. In this, there is another ngController, called "ModalDlgController". Here is the code for this new controller:

var module = angular.module("sampleModalModule", []);
module.controller("ModalDlgController", ["$scope", function ($scope) {
   $scope.someValueField = "";
   
   $scope.modalButtonClick = function () {
      console.log("do action on Modal");
      console.log("Current 'someValueField' value is [[" + $scope.someValueField + "]]");
   };
}]);

In this controller "ModalDlgController", there is a scoped variable called "someValueField". It is bound to a input field on the template via ng-model. Any change to this scoped variable, it will reflect on display. On the template, there is also a button that can be pressed. This button does not do much. There is a scoped function in the controller called "modelButtonClick()". All this function does is output the current value of the scoped variable "someValueField" to the developer console.

Now that we have all the pieces of the sample application, I can show you the addition needed to make the modal dialog displayed.

The First Sample Application

After you download the sample source code, then unzip, you will see three different folders. They each contain a working AngularJS application. Each shows an addition to the previous working sample. This first one is in the folder "app1". In this sample, the problem I like to solve is to pop open the modal dialog.

According to the Bootstrap documentation. To pop open a modal dialog, all it needed is to query the element of the modal dialog, then call the attached JavaScript function called modal(), and pass in parameter "show". This is the code thar roughly does it:

$("#modalDlgId").modal("show");

However, the application architecture is not JQuery. It is AngularJS, which supports JQuery to a certain degree. AngularJS has its own implementation of JQuery called jqlite. It is possible to use AngularJS to query the modal dialog as an element, then call the atached JavaScript function modal() on this element.

Remember the comment I put in the main controller "AppController"? Here is my code that query the modal dialog and pop it up for display:

var dlgElem = angular.element("#modalDlg");
if (dlgElem) {
   dlgElem.modal("show");
}

Here is the entire app.js:

var app = angular.module("sampleAngularApp", [ "sampleModalModule" ]);
app.controller("AppController", ["$scope", function ($scope) {
   $scope.openDlg = function () {
      console.log("clicked here...");
      
      var dlgElem = angular.element("#modalDlg");
      if (dlgElem) {
         dlgElem.modal("show");
      }
   };
}]);

AngularJS provided some helper methods. one of them is the angular.element(), whch works like JQuery selector. In this case, I use it to find an element with the id "modalDlg". The "if" block checks whether the returned element is not null. Then I use the element to call the method modal(). Note that the function modal() is attached to the element by JQuery. The call would show the modal dialog.

Here is a screenshot when you click on the button and show the modal dialog:

You can do a simple test, type something on the text field on the popup modal dialog. Then close it. Open the same modal dialog again, you will see the input you have typed is still there. In a lot of scenarios, this is an unacceptable behavior. The proper behavior is that when the modal dialog closes, it will reset all its child inputs.

The Second Sample Application

The next objective is to figure out a way to clean up the input fields on the modal dialog when it opens or closes. Now that we know there are AngularJS has a helper method called element() that can query elemens on the page, we can apply the same principle to this problem. Bootstrap modal dialogs has event callback that can be used to clean up.

According to the Bootstrap documentation, the callback event when a modal dialog closes can be handled as the following:

$('#exampleModal').on('hide.bs.modal', function (event) {
...
});

There it is, the solution is as the following:

  • Get the element of the modal dialog.
  • Attach the above event call back to this element. In it, clean up the input fields in the modal dialg.

The second sample is in the folder "app2". On top of the change done in "app1", I added the event call back for modal dialog when close it. It is in the ModalDlgController. Here is the code:

var module = angular.module("sampleModalModule", []);
module.controller("ModalDlgController", ["$scope", function ($scope) {
   $scope.someValueField = "";
   
   var dlgElem = angular.element("#modalDlg");
   if (dlgElem) {
      dlgElem.on("hide.bs.modal", function() {
         $scope.someValueField = "";
         console.log("reset data model..");
      });
   }
   
   $scope.modalButtonClick = function () {
      console.log("do action on Modal");
      console.log("Current 'someValueField' value is [[" + $scope.someValueField + "]]");
   };
}]);

The code segment that setsup the event callback is this:

var dlgElem = angular.element("#modalDlg");
if (dlgElem) {
   dlgElem.on("hide.bs.modal", function() {
      $scope.someValueField = "";
      console.log("reset data model..");
   });
}

The idea is the same from the previous section, first I use angular.element() to find the element of the dialog. Then I use it to call the on() method (also from JQuery) to setup the event callback. What is nice about this is that in the event callback, I can actually set the scoped variable to empty string. This is how to reset the input fields on the dialog.

To test, one can open the dialog, enter some value to the input field in the dialog, then close it. Reopen the modal dialop again, the original input value should be gone.

The last technical issue I like to resolve is that somehow I must be able to pass value (or values) from the main app to the modal dialog. Essentially, I like to pass values from my main controller to a child controller or a non-child controller. Turned out, it is also pretty easy to do.

The Third Sample Application

I have created a third app, to demonstrate the way of passing data from one AngularJS controller to another AngularJS controller. Before I continue, I just want to point out that this might not be the best solution, because it might create tight coupling when you want to avoid such thing.

The sample code can be found in folder "app3".

How do you pass values from one controller to another. Turned out with AngularJS, it is quite easy to achieve. The way I have done is first querying the element. Once I found the element, I call .scope() on the element. It will return the AngularJS scope that is attached to the element. Then I can pass any value to the scoped variable in this scope. Here is the whole file of app.js:

var app = angular.module("sampleAngularApp", [ "sampleModalModule" ]);
app.controller("AppController", ["$scope", function ($scope) {
   $scope.openDlg = function () {
      console.log("clicked here...");
      
      var dlgElem = angular.element("#modalDlg");
      if (dlgElem) {
         dlgElem.scope().someValueField = "This text is from the caller.";
         dlgElem.modal("show");
      }
   };
}]);

The segment of interest is this:

var dlgElem = angular.element("#modalDlg");
if (dlgElem) {
   dlgElem.scope().someValueField = "This text is from the caller.";
   dlgElem.modal("show");
}

What is being done here is:

  • Query the element of the modal dialog.
  • Check and make sure the element exist.
  • Call .scope() of the element, which returns the scope which the element is attached to. Then reference to the scoped variable and assign new value to it.
  • Pop open the modal dialog.

If everything works, when you test it, you should see the value that was passed from this main app controller to the modal dialog. And if you change the value then close the modal dialog. Open it again, you should see the value reset to the one that passed from the main app.

Running the Demo Apps

In this tutorial, I have introduced three different solutions to three simple technical problems. The first is to open a bootstrap modal dialog in an AngularJS app. The second one shows how to clean up the input field of a modal dialog when it closes, it can be easily modified to clean up before the modal dialog opens. The last example is to show how to pass data from one controller to another so that the main app controller can pass some data to the modal dialog.

Once you have downloaded the sample source code and unzip it, you will find the three app folders, app1 to app3. In it, you should find a number of *.sj files. These files should be renamed to *.js files.

Also, the demo source code do not include AngularJS and bootstrap source code. Please download them and add them in yourself. The app.html in each of the app folder should show you how. If you encournters any errors, please check the developer's console for more detail.

I use Jetty Web Server to test it. I wrote another article for CodeProject that discuss the approaches of using Jetty Web Server to serve static web content. For testing this demo application, I used the second approach described in that article.

Here is what you do:

  • Download the Jetty Web Server, and unzip it.
  • In the base directory of the Jetty Web Server, find the webapps folder.
  • Unzip the sample app archive. You will find a folder called angular-bootstrap-modal. Inside this folder, there are three sub-folders app1 to app3, the last one is WEB-INF.
  • Copy the folder angular-bootstrap-modal into Jetty's webapp folder.
  • Assuming you have renamed *.sj files to *.js. And added AngularJS and bootstrap code files. If not, then it is time to do these.
  • Run Jetty Web Server with command java -jar start.jar.

Once the web server started up. you can navigate to:

http://localhost:8080/angular-bootstrap-modal/app1/app.html
http://localhost:8080/angular-bootstrap-modal/app2/app.html
http://localhost:8080/angular-bootstrap-modal/app3/app.html

Retrospective

Are these three good solutions to the technical problems? I don't think they are. And I do caution you to reconsider if you want to apply these to solve your specific problem at hand. The first approach that pops open the modal dialog, it breaks encapsulation. The caller component should not know the implementation detail of the component used. But the way I have done it, breaks this rule. The third approach creates tight coupling between the caller component and the component being used. The second approach for cleaning the input field is slightly better, yet there is a coupling between the UI and the back end JavaScript code.

These are not the best ways to use bootstrap components. It is the reason there is ui-bootstrap. I would rather get ui-bootstrap working with my project than using these as alternative solutions. But, they are cool to know.

History

2/2/2018 - Initial Draft

2/25/2018 - Published to han-sulu.com