How to Create a Dynamic Modal Directive in Minutes using Angular and UI Bootstrap

image-post mask This talk I will walk through creating a modal window that is generic in the sense that it will take any old arbitrary html and stuff it in a modal window that is used throughout your site. This uses the Angular Bootstrap UI Modal window and wraps that directive in another directive. You can find the source code to a demo that I have created for this here. I have broken this down into the major sections in which, hopefully, this will be explained. You can either follow along or just look at the code located on the github link above. So let's get started.

GitHub Demo Repo

Generic-Modal

Modal Directive's Structure

image

Like described above my modal directive wraps the Angular-UI-Bootstrap modal.

The Need for a Generic Modal Window

Ok. So let's start with my use case for a modal window. I have a website, and this website needs to have about 15 modal windows. Because this website does not have any navigation all information, other than that on the main page, needs to use a modal window. For example, an author's bio or an excerpt from their book. So I have two options 1. I code one modal window for every instance I need, or 2. Define a directive that allows me to create a modal window and shove any old HTML partial in. Guess what I chose?

Derective Definition Object(DDO)

The directive took a couple of iterations to create, and still could use some improvements, but overall it fills the use case very well.

module.directive('myModal', function($modal){
return {
    transclude: true,
    restrict: 'EA',
    template: '<a ng-click="open()" ng-transclude>{{name}}</a>',
    scope: {
        useCtrl: "@",
        email: "@"
    },
    link: function(scope, element, attrs) {
        scope.open = function(){

            var modalInstance = $modal.open({
                templateUrl: templateDir+attrs.instanceTemplate +'.tpl.html',
                controller:  scope.useCtrl,
                size: 'lg',
                windowClass: 'app-modal-window',
                backdrop: true,
                resolve: {
                    custEmail: function(){
                        return {email: scope.email};
                    }
                }
            });
            modalInstance.result.then(function(){
                console.log('Finished');
            }, function(){
                console.log('Modal dismissed at : ' + new Date());
            });
        };
    }
};
});

So listed above is the main directive also known as the Derective Definition Object(DDO). This basically creates an HTML element named myModal. There are 3 important sections to note here. First, we have the section:

 return {
    transclude: true,
    restrict: 'EA',
    template: '<a ng-click="open()" ng-transclude>{{name}}</a>',
    scope: {
        useCtrl: "@",
        email: "@"
    },

This code allows the outer HTML file to use the myModal element or attribute. This is allowed by listing the restrict to EA. Also, it allows the myModal element to list attributes to be passed into the directive.

Tranclusion

Now I added transclusion tag to the JSON object becuase I needed to be able to use the HTML that was inside of the myModal element. One use case was that I needed to make an image a link for the modal to open. For example, take this HTML snippet:

<myModal>
    <img src="img/content/car.png"/>
</myModal>

Now when this is compiled by Angular's HTML compiler it will insert the inner HTML of myModal tag into the template tag that is defined above, which is just a wrapper for an anchor tag to that calls the open function of the modal instance.

Isolate Scope

Isolate scope is definitely something I needed, since I will be opening up a ton of these and if they all had the same scope the last one to be written to the outer HTML page would overwrite all the previous listed myModal tags. So above I used an isolate scope that recieves 2 attributes from the myModal element. First, is probably the most important and that defines which controller you want your modal to use. Second, is an email address. Now the email address attribute can be totally removed from this directive, but I thought I would show an example of passing information from the encasing HTML to the directive then to the modal instance's controller, which I will go over in depth in the [Passing Data from Main Page to the Instance Controller] [passingData]. Now time to talk about how I wrapped Angular UI Boostrap's modal instance inside my own directive.

Ecapsulating the Bootstrap UI's Modal Instance Service

Instead of creating my own modal window I saw that Angular UI Bootstrap has its own single modal directive. So instead of reinventing the wheel for the modal I went ahead and used theirs.

 scope.open = function(){


            var modalInstance = $modal.open({
                templateUrl: templateDir+attrs.instanceTemplate +'.tpl.html',
                controller:  scope.useCtrl,
                size: 'lg',
                windowClass: 'app-modal-window',
                backdrop: true,
                resolve: {
                    custEmail: function(){
                        return {email: scope.email};
                    }
                }

            });

            modalInstance.result.then(function(){
                console.log('Finished');
            }, function(){
                console.log('Modal dismissed at : ' + new Date());
            });
        };

If you are not familiar with Angular UI Bootstrap I highly suggest reviewing it, so you can know how the UI Boostrap modal instance works and all of its other options which you can pass into the instance.

Since this is placed into the linking method in the directive it specifies what actions you want your new HTML 'myModal' elements to do. For this case, we want it to pop up a modal window, and thats just what this does. Basically, the outer HTML has an ng-click='open()' and this then just calls the open function on the UI Bootstrap's modal instance directive. Now this directive takes a couple of options/parameters. Now, like I said above, there are around 15 options you can use for UI Boostrap's modal, so if you need to customize you modal window to do other things check them out first. I will only go over the ones that are contextable to my directive, which is the controller,template, and resolve.

Specifying the Template/HTML to use

The template that you would like to use is passed in as an attribute included in the myModal element. For example,

<my-modal name='RedBull' instance_template="redbull" use_ctrl="RedBullCtrl">
    RedBull
</my-modal>

This instance-template attribute is actually the name of the template you want to display inside of the modal window. Now my directive looks for templates in the /views/templates/ directory, which is defined in the angular module which holds the directive, but you can specify it as anywhere you keep your templates. In addition, I put the .tpl.html extension inside the directive, so I can make my markup more declaritive then have to put it on within the HTML.

Specifying the Controller

The controller for your modal instance is done with an attribute. This is just like declaring the template except this time the attribute is use_ctrl. For example, if I wanted to use RedBullCtrl I would just declare it as use_ctrl='RedBullCtrl. This is all shown in the HTML snippet above. Now remember I am wrapping the modal window instance, which creates its own scope for each modal instance controller. This is both good and bad. First, the good. Since I declared my modal window with isolate scope each scope is separate from another, which allows us to reuse the same controller if we wanted to. Now the bad. I have about 20 modal windows. Each one has its own scope and sometimes a child scope. This means I might have 30 or more scopes to deal with. Ok, so it wasn't too bad.

Passing Data from Main Page to the Instance Controller

Now say we have information which we need to pass from the HTML that houses our modal window to our directive then to our modal instance where we can use it in our modal instance's scope.

Attributes...Attributes...Attributes

Once again we jump to the help of attributes within our modal. Now just a forewarning, when you start to add attributes to your modal to pass relevant information about a type of modal your making you start making your directive specific instead of generalized. So maybe there is a better way to do this, but this is what I did to pass information. First, you declare an attribute in the DDO isolate's scope like so:

 scope: {
        useCtrl: "@",
        email: "@"
    },

Here I declare three attributes displayed with an @ sign this tells the isolate scope to pick up the attribute from the HTML and stuff it in here. So useCtrl is obviously the attribute I just talked about. The next two are what we are concerned with. In combination with these attributes and the resolve option used by UI-Bootstrap's modal instance we can pass our attribute values straight into the modal instance's controller like so:

  var modalInstance = $modal.open({
                templateUrl: templateDir+attrs.instanceTemplate +'.tpl.html',
                controller:  scope.useCtrl,
                size: 'lg',
                windowClass: 'app-modal-window',
                backdrop: true,
                resolve: {
                    custEmail: function(){
                        return {email: scope.email};
                    }
                }

            });

Add it to the Modal Instance Controller

Now you can just add this to the modal instance's controller like this:

module.controller('EmailAuthorCtl', function($scope, $modalInstance, $http, custEmail){
...
}

And thats it!! Now you have a generic modal window which you can reuse all over your application, so you dont have to recreate one everytime. Of course I have my demo listed in my generic-modal github repo, which includes all the source code you need to make your own generic modal window.

Comment Please

Please leave comments/recommendations or even code improvement suggestions, as I am sure this can be refactored

Up Next

Compiling HTML on the Fly via $compileProvider Directive
Winston Logger in NodeJS - Chiiillll Winston, I'm Just Blogging on Logging
NPM/Bower Link - Make Friends with Your Current Dev Node Modules and Bower Components Today