Building an AngularJS Dialog Service

Update: The Angular UI Bootstrap project pulled the $dialog service due to some incompatibility issues with the latest version of Bootstrap (hopefully it'll be back). In the meantime check out my post on Building an AngularJS Modal Service to work around the change.

If you’re new to AngularJS check out my AngularJS in 60-ish Minutes video tutorial or download the free eBook. Also check out The AngularJS Magazine for up-to-date information on using AngularJS to build Single Page Applications (SPAs).

 

 

One of the tasks I find myself performing a lot when building Line of Business (LOB) applications is displaying dialogs. Some of the dialogs are modal (the user can’t click on the background to dismiss it) and some are non-modal. For example, I’m currently working on a LOB application that displays modal dialogs as users delete items on a page:

 

image

 

I’ve tried a few different AngularJS dialog directives that are out there but recently switched to Angular UI Bootstrap since its directives don’t have any dependencies on external libraries and are easy to use. You can show a dialog directly from a controller by injecting their $dialog object. Here’s an example directly from their site that demonstrates how to display a dialog that’s based on a template:

function DialogDemoCtrl($scope, $dialog) {

    // Inlined template for demo
    var t = '<div class="modal-header">' +
            '<h3>This is the title</h3>' +
            '</div>' +
            '<div class="modal-body">' +
            '<p>Enter a value to pass to <code>close</code> as the result: <input ng-model="result" /></p>' +
            '</div>' +
            '<div class="modal-footer">' +
            '<button ng-click="close(result)" class="btn btn-primary" >Close</button>' +
            '</div>';

    $scope.opts = {
        backdrop: true,
        keyboard: true,
        backdropClick: true,
        template: t, // OR: templateUrl: 'path/to/view.html',
        controller: 'TestDialogController'
    };

    $scope.openDialog = function () {
        var d = $dialog.dialog($scope.opts);
        d.open().then(function (result) {
            if (result) {
                alert('dialog closed with result: ' + result);
            }
        });
    };
}

function TestDialogController($scope, dialog) {
    $scope.close = function (result) {
        dialog.close(result);
    };
}

 

Looking through the code you can see that the $dialog object’s dialog() function can be called to create a dialog. The dialog can then be shown by calling the open() function. While this code is easy to add into a controller, I found myself adding similar code across multiple controllers. Any time that happens I know it’s time to refactor and encapsulate code into a factory or service.

 

Creating a Dialog Service


AngularJS services provide a nice way to build a singleton object that can be re-used across multiple controllers or other factories/services. Since I was writing similar dialog code across multiple controllers, creating a re-useable dialog service made sense given that services provide a great way to maximize re-use. Keep in mind that I could’ve chosen a factory as well since factories can do the same thing as services in AngularJS (only the way the singleton is created by a factory/service actually differs between the two). See my previous post for an example of a factory and a service that accomplish the same task.

To create a custom AngularJS service I started out by including the Angular UI Bootstrap script (available here) into my main shell page. I generated a custom version of the script since I didn’t need all of the directives that they have available:

<script src="/Scripts/angular.js"></script>
<script src="/Scripts/angular-route.js"></script>
<script src="/Scripts/angular-animate.js"></script>
<script src="/Scripts/angular-ui-custom.js"></script>


Next I added a reference to the Angular UI Bootstrap module (called ui.bootstrap) into my custom module definition:

 

angular.module('myApp', ['ngRoute', 'ngAnimate', 'ui.bootstrap']);

 

The next step was to define a re-useable template that covered the majority of the dialogs that I wanted to show. I named the template dialog.html and placed it in app/partials within my project’s folder structure.


<div class="modal-header">
  <h3>{{dialogOptions.headerText}}</h3>
</div>
<div class="modal-body">
  <p>{{dialogOptions.bodyText}}</p>
</div>
<div class="modal-footer">
  <button type="button" class="btn" 
data-ng-click="dialogOptions.close()">{{dialogOptions.closeButtonText}}</button> <button class="btn btn-primary"
data-ng-click="dialogOptions.callback();">{{dialogOptions.actionButtonText}}</button> </div>


The template could certainly be added directly into the service to minimize the number of required files, but I’m not a big fan of adding HTML unless it’s for a directive so I decided to keep it separate – at least for now. You can see that the template binds to a dialogOptions object that provides the header and body text as well as the text for the buttons. dialogOptions also provides close() and callback() functions which you’ll see in use a little later in this post.

From there I created a service named dialogService. Here’s the shell for the service:


angular.module('myApp').service('dialogService', ['$dialog', 
    function ($dialog) {


}]);


Where did $dialog come from? That’s a service provided by Angular UI Bootstrap’s ui.bootstrap module that was mentioned earlier. You can use the $dialog service directly within controllers which is what I originally did. However, as mentioned, I found myself duplicating some code and decided to abstract it out into a custom service as a result.

The first part of the dialogService defines some defaults for $dialog  as well as a dialogOptions object (mentioned earlier in the template) that defines the default text for the header, body, and buttons:

 

var dialogDefaults = {
    backdrop: true,
    keyboard: true,
    backdropClick: true,
    dialogFade: true,
    templateUrl: '/app/partials/dialog.html'
};

var dialogOptions = {
    closeButtonText: 'Close',
    actionButtonText: 'OK',
    headerText: 'Proceed?',
    bodyText: 'Perform this action?'
};

 

Although the defaults work in some scenarios, there are times when customizations are required. To accomplish this I created a showDialog() function that maps/extends custom defaults and dialog options to the ones shown above:

 

this.showDialog = function (customDialogDefaults, customDialogOptions) {
    //Create temp objects to work with since we're in a singleton service
    var tempDialogDefaults = {};
    var tempDialogOptions = {};

    //Map angular-ui dialog custom defaults to dialog defaults defined in this service
    angular.extend(tempDialogDefaults, dialogDefaults, customDialogDefaults);

    //Map dialog.html $scope custom properties to defaults defined in this service
    angular.extend(tempDialogOptions, dialogOptions, customDialogOptions);

    if (!tempDialogDefaults.controller) {
        tempDialogDefaults.controller = function ($scope, dialog) {
            $scope.dialogOptions = tempDialogOptions;
            $scope.dialogOptions.close = function (result) {
                dialog.close(result);
            };
            $scope.dialogOptions.callback = function () {
                dialog.close();
                customDialogOptions.callback();
            };
        }
    }

    var d = $dialog.dialog(tempDialogDefaults);
    d.open();
};


The first part of this function maps the custom defaults and options to the service defaults using angular.extend(). Since a service is a singleton I rely on two temp objects named tempDialogDefaults and tempDialogOptions to hold the data so that each call to showDialog() results in unique customizations being applied to dialogs. I do this because the dialogDefaults and dialogOptions objects are shared across all callers so I can’t update those directly without affecting other callers.

The code first maps the dialogDefaults in the service to tempDialogDefaults. Any customizations passed into the function are then mapped/extended to the tempDialogDefaults. This is an extra mapping/extending step, but it gets the job done in a singleton scenario. The same process is used to map/extend the dialog options.

From there the code checks if a controller was provided by the caller. If a controller wasn’t provided a new one is created. Inside of the controller the dialog options are assigned to the scope (recall that these are used in the template shown earlier to define header, body, and button text) and functions for close() and callback() are also defined. The caller can pass in a custom callback function that will be invoked once the main button is clicked in the dialog. I considered using $q here with a promise and may switch to that in the future, but for now allowing a callback to be passed in gets the job done.

The final part of code creates a new dialog using the $dialog service’s dialog() function and then handles showing it by calling open().

The showDialog() function in the service shows a non-modal dialog unless the caller passes in a value of false for a backdropClick property recognized by  $dialog. To make it easy to work with modal dialogs I added a showModalDialog() function that sets the backdropClick value and then calls the showDialog() function shown earlier:

 

this.showModalDialog = function (customDialogDefaults, customDialogOptions) {
    if (!customDialogDefaults) customDialogDefaults = {};
    customDialogDefaults.backdropClick = false;
    this.showDialog(customDialogDefaults, customDialogOptions);
};


The final function added into dialogService is showMessage(). This function handles showing a standard message type of box (as opposed to a dialog) that is closed by clicking the page background or the “OK” button displayed in the message. It relies on a template built-into Angular UI Bootstrap named template/dialog/message.html and a controller that’s also built-in named MessageBoxController. The showMessage() function accepts the title and message to display as well as a buttons object that can be used to display the appropriate text for the button shown in the message as well as the CSS applied to the button.

 

this.showMessage = function (title, message, buttons) {
    var defaultButtons = [{result:'ok', label: 'OK', cssClass: 'btn-primary'}];
    var msgBox = new $dialog.dialog({
        dialogFade: true,
        templateUrl: 'template/dialog/message.html',
        controller: 'MessageBoxController',
        resolve:
                {
                    model: function () {
                        return {
                            title: title,
                            message: message,
                            buttons: buttons == null ? defaultButtons : buttons
                        };
                    }
                }
    });
    return msgBox.open();
};


The complete dialogService code is shown next:

 

angular.module('myApp').service('dialogService', ['$dialog', 
    function ($dialog) {

        var dialogDefaults = {
            backdrop: true,
            keyboard: true,
            backdropClick: true,
            dialogFade: true,
            templateUrl: '/app/partials/dialog.html'
        };

        var dialogOptions = {
            closeButtonText: 'Close',
            actionButtonText: 'OK',
            headerText: 'Proceed?',
            bodyText: 'Perform this action?'
        };

        this.showModalDialog = function (customDialogDefaults, customDialogOptions) {
            if (!customDialogDefaults) customDialogDefaults = {};
            customDialogDefaults.backdropClick = false;
            this.showDialog(customDialogDefaults, customDialogOptions);
        };

        this.showDialog = function (customDialogDefaults, customDialogOptions) {
            //Create temp objects to work with since we're in a singleton service
            var tempDialogDefaults = {};
            var tempDialogOptions = {};

            //Map angular-ui dialog custom defaults to dialog defaults defined in this service
            angular.extend(tempDialogDefaults, dialogDefaults, customDialogDefaults);

            //Map dialog.html $scope custom properties to defaults defined in this service
            angular.extend(tempDialogOptions, dialogOptions, customDialogOptions);

            if (!tempDialogDefaults.controller) {
                tempDialogDefaults.controller = function ($scope, dialog) {
                    $scope.dialogOptions = tempDialogOptions;
                    $scope.dialogOptions.close = function (result) {
                        dialog.close(result);
                    };
                    $scope.dialogOptions.callback = function () {
                        dialog.close();
                        customDialogOptions.callback();
                    };
                }
            }

            var d = $dialog.dialog(tempDialogDefaults);
            d.open();
        };

        this.showMessage = function (title, message, buttons) {
            var defaultButtons = [{result:'ok', label: 'OK', cssClass: 'btn-primary'}];
            var msgBox = new $dialog.dialog({
                dialogFade: true,
                templateUrl: 'template/dialog/message.html',
                controller: 'MessageBoxController',
                resolve:
                        {
                            model: function () {
                                return {
                                    title: title,
                                    message: message,
                                    buttons: buttons == null ? defaultButtons : buttons
                                };
                            }
                        }
            });
            return msgBox.open();
        };

    }]);


Using dialogService

 

Now that dialogService is in place it can be injected into any controller and re-used over and over. Here’s an example of a controller that uses it:

 

angular.module('myApp').controller('myController', ['$scope', '$routeParams', '$location',  'dialogService', 
        function ($scope, $routeParams, $location, dialogService) {


}]);

 

A simple example of using dialogService to show the standard dialog is shown next:

 

dialogService.showModalDialog();

 

image

 

To show a custom message the following code can be used:

 

dialogService.showMessage('Record not Found!', 'The record you were looking for cannot be found.');

 

image

 

In cases where the dialog needs to be customized, several different options can be set. Here’s an example of customizing just about every aspect of the dialog template discussed earlier:

 

$scope.delete = function () {
    var dialogOptions = {
        closeButtonText: 'Cancel',
        actionButtonText: 'Delete Timesheet',
        headerText: 'Delete Timesheet?',
        bodyText: 'Are you sure you want to delete this timesheet?',
        callback: function () {
            recordsService.delete($scope.record.id)
                .success(function (opStatus) {
                    if (opStatus.status) {
                        $location.path('/records');
                    }
                    else {
                        errorService.createErrorAlert($scope, 
'There was a problem deleting the timesheet: ' + error.message); } }) .error(function (error) { errorService.createErrorAlert($scope, 'Error deleting record: ' + error.message); }); } }; dialogService.showModalDialog({}, dialogOptions); };

 

image

 

Summary


AngularJS provides several different ways to encapsulate functionality and make it more re-useable across an application. Anytime I find myself duplicating code across controllers I like to pull it out into a factory or service. Understanding the different ways you can use factories/services will definitely help you out as you build AngularJS applications. Although this is a pretty basic wrapper around the AngularJS UI Bootstarp $dialog service I hope it gets you started working with services and dialogs in AngularJS.

Have improvements you see can be made? At some point I’ll get this code into a sample project hosted on GitHub, but for now feel free to leave a comment.

 

Check out my other posts on AngularJS.

Published Monday, August 19, 2013 8:10 PM by dwahlin
Filed under: , ,

Comments

# re: Building an AngularJS Dialog Service

Tuesday, August 20, 2013 2:42 AM by Marco

As always a great post. Thanks!

But can you have a look at your RSS feed, please? Its quite hard to read the examples, as HTML tags are inserted into the javascript code (for formatting for the web view). It would be better if HTML tags could be stripped from the code examples.

Thanks!

# Dew Drop &ndash; August 20, 2013 (#1,607) | Alvin Ashcraft&#039;s Morning Dew

Pingback from  Dew Drop &ndash; August 20, 2013 (#1,607) | Alvin Ashcraft&#039;s Morning Dew

# re: Building an AngularJS Dialog Service

Tuesday, August 20, 2013 12:55 PM by Matt B.

Where did error.message message come from in your delete timesheet function?

# re: Building an AngularJS Dialog Service

Tuesday, August 20, 2013 1:03 PM by dwahlin

Matt:

The error message code is a custom errorService in the actual app that is called if the delete() call fails and the error() branch is invoked. It just handles logging errors and things.

Dan

# re: Building an AngularJS Dialog Service

Tuesday, August 20, 2013 1:12 PM by dwahlin

Marco:

Thanks for checking out the post. As far as the RSS, I'll look into that. I use a formatting tool so the code looks good in the browser and is highlighted which is why there are HTML tags mingled in with the JavaScript. While I could take that out, all of the highlighting would be lost unfortunately. If you view it directly (http://feeds2.feedburner.com/dwahlin) it should look pretty good, but some RSS readers may not handle displaying it correctly.

Dan

# re: Building an AngularJS Dialog Service

Tuesday, August 20, 2013 3:08 PM by Marco

Dan:

I know where the tags come from and I understand that its very useful for your webview.

The problem is that the HTML tags are escaped when getting the RSS feed and so I only see javascript with mingled in html tags in chrome (!) when reading your posts via feedly.

# re: Building an AngularJS Dialog Service

Tuesday, August 20, 2013 3:44 PM by dwahlin

Marco:

That's definitely a bummer and pretty useless I'd agree for reading code. I don't want to eliminate the highlighting at this point but if I come up with a work-around I'll be happy to implement it.

Thanks,

Dan

# Building an AngularJS Dialog Service - Dan Wahl...

Tuesday, August 20, 2013 4:33 PM by Building an AngularJS Dialog Service - Dan Wahl...

Pingback from  Building an AngularJS Dialog Service - Dan Wahl...

# re: Building an AngularJS Dialog Service

Wednesday, August 21, 2013 12:59 PM by Mike Erickson

Hey There

Is this code available for download anywhere?

# re: Building an AngularJS Dialog Service

Wednesday, August 21, 2013 1:38 PM by Mike Erickson

It should be noted, this code will ONLY work Bootstrap 2.3.x as the ui.bootstrap service has not been updated as of the time of this comment to work with Bootstrap 3.x   There are some repos in existence which have been modified to work with BS3, but they are not yet released for general public consumption.

I am sure it is just a matter of time before the ui.bootstrap services are updated to work BS3 in public forum, but wanted to save some with the headache in case you try it with BS3.

# Building an AngularJS Dialog Service - Dan Wahl...

Wednesday, August 21, 2013 3:11 PM by Building an AngularJS Dialog Service - Dan Wahl...

Pingback from  Building an AngularJS Dialog Service - Dan Wahl...

# re: Building an AngularJS Dialog Service

Wednesday, August 21, 2013 3:17 PM by dwahlin

Mike:

Good point since Bootstrap is moving to V3. As far as the code, I don't have anything available right now to push up to Github (it's in a real client project) but I included the complete service above if you want to do a good old cut and paste. :-)

Dan

# Building an AngularJS Dialog Service | AngularJ...

Thursday, August 22, 2013 3:13 AM by Building an AngularJS Dialog Service | AngularJ...

Pingback from  Building an AngularJS Dialog Service | AngularJ...

# The AngularJS Magazine – What’s New this Week?

Friday, August 23, 2013 7:57 PM by Dan Wahlin

&#160; Click to view the FlipBoard magazine &#160; AngularJS Magazine Updates for August 23rd Configuring

# The AngularJS Magazine – What’s New this Week?

Tuesday, September 3, 2013 10:10 AM by Dan Wahlin

&#160; Click to View &#160; AngularJS Magazine Updates for September 30th Custom Validations in AngularJS

# Pro ASP.NET Web API: HTTP Web Services in ASP.NET view | WWW.MYTOURISMBLOG.COM

Pingback from  Pro ASP.NET Web API: HTTP Web Services in ASP.NET view | WWW.MYTOURISMBLOG.COM

# The AngularJS Magazine – What’s New this Week?

Tuesday, September 17, 2013 12:38 AM by Dan Wahlin

&#160; Click to View Here’s what’s new in the world of AngularJS. Check out the Flipboard magazine above

# The AngularJS Magazine ??? What???s New this Week? | enCaliber

Saturday, September 21, 2013 4:59 AM by The AngularJS Magazine ??? What???s New this Week? | enCaliber

Pingback from  The AngularJS Magazine ??? What???s New this Week? | enCaliber

# re: Building an AngularJS Dialog Service

Friday, September 27, 2013 10:27 AM by d3maybe

Dan, is there a way to customize the dialog to have possible inputs... say I want to ask for a email address or scan input from a scan gun or something like that? maybe pass in a callback to get the value in the originating controller?

# re: Building an AngularJS Dialog Service

Wednesday, October 2, 2013 10:29 AM by Starter

Do you have this sample available to download?

Best regards

# re: Building an AngularJS Dialog Service

Wednesday, October 2, 2013 3:03 PM by dwahlin

Check out the modal service post mentioned at the beginning of the post. The $dialog service was removed but the $modal service is still around and I mention a sample app where you can see it at the bottom of the post.

Dan

# re: Building an AngularJS Dialog Service

Saturday, October 26, 2013 5:28 PM by David

Hi, can you also make a tutorial on doing the same on Hubspot's vex?

# Factory | Pearltrees

Thursday, January 9, 2014 2:21 PM by Factory | Pearltrees

Pingback from  Factory | Pearltrees