AngularJS Tutorial - 6.1

In this part we will discover how to build a dialog service.

Screenshot

Download

The source for this part is here: 06_dialog.zip.

The library created based on this tutorial is on the sgDialogService

The samples can be run with Firefox, in general or by running, into the directory,

The dialog - Without Angular-UI

Define the global template

We will follow the standard dialog template from Bootstrap. This will be saved as "app/common/dialog.html" The items surrounded with sharps will be replaced by the service when creating the real dialog.

Note the modalButtons inside the modal footer. Those must be created into the controller.

<div class='modal-dialog' id='modalDialog' name='modalDialog'>
    <div class='modal-content '>
        <div class='modal-header'>
            <button type='button' class='close' ng-click='#closeModal#' aria-label='Close'><span aria-hidden='true'>×</span></button>
            <h4 class='modal-title' >#modalTitle#</h4>
        </div>
        <div class='modal-body'>
            #modalBody#
        </div>
        <div class='modal-footer' >
            <button ng-repeat='button in modalButtons' type='button' 
                class='btn {{button.class}}'  
                ng-click='button.action()' ng-disabled="button.disabled()">{{button.text}}</button>
        </div>
    </div>
</div>

CSS

This is the css that will handle the dialog (mixed with the bootstrap css). Note the media query. This will allow the dialog to fill the viewport if, for example, the page is viewed on a mobile.

The modalHidden class is used to show or hide the whole dialog and overlay.

@media (max-width: 600px) {
    .modal-dialog {
        top: 0;
        right: 0;
        bottom: 0;
        left: 0;
    }

    .modal-content {
        top: 0;
        right: 0;
        bottom: 0;
        left: 0;
    }
}

.modalDialogOverlay {
    position: fixed;
    font-family: Arial, Helvetica, sans-serif;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
    background: rgba(0,0,0,0.2);
    z-index: 1000;
    opacity:.8;
    -webkit-transition: opacity 400ms ease-in;
    -moz-transition: opacity 400ms ease-in;
    transition: opacity 400ms ease-in;
    pointer-events: auto;
}

.modalHidden{
    display:none;
    pointer-events: none;
}

The dialog service

We will create for this a service, called "globalModalService". The dependencies will be:

common.service('globalModalService',['$http', '$templateCache', '$controller', '$compile','$rootScope','$document',
    function($http, $templateCache, $controller, $compile,$rootScope,$document) {
    ...

Then the variables needed. Modal instance will contain the tipical parts of the dialog, used by the dialog controller.

        //Initial position of the floating dialog
        var startX, startY;
        //Position where the mouse clicked on the dialog
        var initialMouseX, initialMouseY;
        //Content of the main template body
        var dialogTemplateBody = "";
        //Instance 
        var modalInstance = {};
        ...

Load the template content into the service local variable. For this resource will be used the templateCache as...cache

        $http.get('app/common/dialog.html', { cache: $templateCache })
            .then(function (response) {
                dialogTemplateBody = response.data;
            });
        ...

The overlay (the grey shade) is created to allow modal operations. First is checked if the overlay is already present The content of the overlay will be compiled into an angular item, and initialized using the rort scope as scope. Then the item is appended to the document body

        ...
        var overlay = document.getElementById("modalDialogOverlay");
        if(!overlay){
            var overlayContent = "<div id='modalDialogOverlay' name='modalDialogOverlay' class='modalDialogOverlay modalHidden'></div>";
            //Setup the overlay and append to context
            var compiledOverlay = $compile(overlayContent)($rootScope);
            document.body.appendChild(compiledOverlay[0]);
        }

Then a function is created to open the modal. We setup the modal instance with the overlay. Will be used later. The setup parameter is an object containing:

        this.openModal = function(setup){
             modalInstance = {
                overlay : angular.element(document.getElementById("modalDialogOverlay"))
             };
             ...

The template is loaded and its content is stored into a variable

            $http.get(setup.template, { cache: $templateCache })
                .then(function (response) {
                    var contentTemplate = response.data;
                    ...

We search for the occurrences of the ng-controller string inside the template. This is needed since we want to control the creation of the scope. If we leave the ng-controller Angular will create a new, not accessible, scope. This is added to find the name of the controller and instantiate it anyways. After having set the controller on the "setup" object any reference to the controller is removed from the template.

                    if(!setup.controller){
                        //Seek the first ng-controller
                        var controller = response.data.match(/ng-controller(\s\S)*=(\s\S)*("|')([^"']*)("|')/mi);
                        if(controller!=null){
                            //Get the name (the 4th match group)
                            setup.controller = controller[4];
                            //And remove it from the content
                            contentTemplate = contentTemplate.replace(controller[0],"");
                        }
                    }

The parameters are initialized with default values if not present. This function will be explained later.

                    defaultParameters(setup);

A new scope is created for the controller

                    templateScope = $rootScope.$new();

We then setup the close function on our "modal instance". When called makes the overlay invisible, execute the callback passed by the setup and destroy the dialog.

                    modalInstance.closeModal = function(){
                        //Make the overlay invisibile
                        modalInstance.overlay.toggleClass('modalHidden');
                        //Remove the dialog
                        modalInstance.dialog.remove();
                        //Execute the optional callback
                        if(setup.callback){
                            setup.callback();
                        }
                    }

Add the close function to the scope (for the close button!) and copy the data on the template. This is made leveraging on the fact that javascript objects are exactly like hashmaps.

                    templateScope[setup.closeModal] = modalInstance.closeModal;
                    
                    for(var prop in setup.data){
                        templateScope[prop] = setup.data[prop];
                    }

Then we initialize the template injecting the dependencies. The first parameter of $controller is the name of the controller. The second parameter is an object containing the name/value pairs of the items that will be passed as dependencies to the controller. Between the parameters there is the scope we just created that will be the scope seen by the controller.

                    if(setup.controller){
                        $controller(setup.controller, { 
                            $scope: templateScope,
                            modalInstance: modalInstance
                        });
                    }

Now we will modify the template with the values passed with setup

                    var realTemplate = dialogTemplateBody.replace("#modalTitle#","{{"+setup.title+"}}");
                    
                    //Set the iterator on the buttons
                    realTemplate = realTemplate.replace("button in modalButtons","button in "+setup.modalButtons);
                    
                    //Insert the function to call to close the modal
                    realTemplate = realTemplate.replace("#closeModal#",setup.closeModal+"()");
                    //Insert the template into the dialog context
                    realTemplate = realTemplate.replace("#modalBody#",contentTemplate);

We then create the content. Compiling the template with all replacements connecting it with the scope. We then toggle the visibility of the overlay to show it and we save the instance of the dialog

                    var element = $compile(realTemplate)(templateScope);
                    //And append at the end of body
                    document.body.appendChild(element[0]);
                    //Show the overaly
                    modalInstance.overlay.toggleClass('modalHidden');
                    
                    //Get the connected angular element
                    modalInstance.dialog = angular.element(element);

Now we will place the dialog into the middle of the screen. We will cover this function later.

                    centerTheDialog();
                });
        }

Now we will add the initalization function:

        var defaultParameters = function(setup){
            if(!setup.data){setup.data = {};}
            if(!setup.title){setup.title = 'title'; }
            if(!setup.modalButtons){setup.modalButtons = 'modalButtons';}
            if(!setup.closeModal){setup.closeModal = 'closeModal';}
        }

And the dialog centering function. The left position will be obtained getting the total width of the page and subtracting the width of the dialog. At the point when it's called the rendering have already complete, so we have the -REAL- size of the dialog,

        var centerTheDialog = function(){
            var w = Math.max(document.documentElement.clientWidth, window.innerWidth || 0);
            pos = w/2-modalInstance.dialog.prop('offsetWidth')/2;
            modalInstance.dialog.css({
                        position: 'absoulte',
                        left:pos+'px'
                    });
        }

Draggable Dialog

First we handle the click on a draggable are (the dialog header). This will be placed after the call to "modalInstance.overlay.toggleClass('modalHidden');". The mousedown is binded on the header, and are binded the mouseup and move on the document.

                    ...
                    modalInstance.overlay.toggleClass('modalHidden');
                    
                    var gripElement = angular.element(element[0].getElementsByClassName('modal-header'));
                                        
                    //Bind the mousemove
                    gripElement.bind('mousedown', function($event) {
                        //Retrieve the absolute offset of the whole item
                        startX = modalInstance.dialog.prop('offsetLeft');
                        startY = modalInstance.dialog.prop('offsetTop');
                        
                        //Get the initiali mouse click
                        initialMouseX = $event.clientX;
                        initialMouseY = $event.clientY;
                        
                        $document.bind('mousemove', mousemove);
                        $document.bind('mouseup', mouseup);
                        return false;
                    });

To move the dialog around the screen. The return false tells the event not to propagate further. The position is calculated based on the starting position, the mouse position (clientX and clientY) and the first position of the mouse click

        var mousemove = function($event) {
                var dx = startX + ($event.clientX - initialMouseX);
                var dy = startY + ($event.clientY - initialMouseY);

                var style = {
                    top:   dy + 'px',
                    left:  dx + 'px'
                };
                //Force the css
                modalInstance.dialog.css(style);
                return false;
            }

At the end the mouse move and up are unbinded.

        var mouseup = function() {
            $document.unbind('mousemove', mousemove);
            $document.unbind('mouseup', mouseup);
        }

Changing the dialog content controllers

First remove all the reference in $routeProvider to other than the list and the default route.

Into the list controller we will add the dependency on globalModalService. We specify then how to open the dialog adding the calls to this functions on the customers/list.html. The parameters for the openModal are:

       
        $scope.editDetail = function(id){
            globalModalService.openModal(
                {
                    template:'app/customers/edit.html',
                    data:{id:id},
                    callback: function(){ $scope.loadData();}
                })
        }
        
        $scope.addNew = function(){...}
        $scope.viewDetail = function(id){...}

Let see the edit detail controller. We add the dependency from modalInstance, that exposes the "closeModal" method. The changes of location towards the list of customers will be changed to close the modal

modalInstance.closeModal();

The parameter are not passed anymore as route parameters

$http.get('/customers/'+$routeParams.customerId)

is replaced, looking at what is passed by the openModal in the data field

$http.get('/customers/'+$scope.id)

Finally we will add the buttons and the title. The button have the following specs

Note specifically that we use the $scope.editForm. That is the form defined inside the edit.html template!

        $scope.title = "Editing customer "+$scope.id;
        $scope.modalButtons =[
            {
                action:function(){modalInstance.closeModal();},
                text:"Cancel",class:"btn-default"
            },
            {
                action:function(){update();},
                text:"Update",class:"btn-primary",
                disabled: function(){ if($scope.editForm)return $scope.editForm.$invalid || !$scope.editForm.$dirty;}
            }
        ];

Changing the dialog content template

We will use exactly the same template used on the point 5 simply adding around the form a div that declares the ng-controller that will be used by the template!

<div ng-controller="CustomerEditController">
    <form novalidate name="editForm" id="edit-form" role="form" ng-submit="update()">
        ...
    </form>
</div>

Last modified on: April 13, 2015