Building Message Dialog with Angular, Bootstrap and TypeScript

I blogged previously about building a generic Please Wait dialog with Twitter Bootstrap.  One other common scenario that comes up in typical business application is the scenario when you need to show some information to the user and ask him to click a button to select an option based on this information.  Essentially I am talking about your basic WinForms dialog.  I am going to describe how to extend please wait dialog class with options to show additional buttons and messages.  I am going to use TypeScript, as I have grown quite fond of this JavaScript extension.  If you download the sample though you will find JS files there as well, compiled from TypeScript.

First of all, I just need to define my two divs – one for please wait dialog, one for messages dialog.  I need to update previously posted code to Bootstrap 3, as some classes changed there for modal dialogs.  Here is updated please wait div– I am setting it up in JavaScript (well, TypeScript, but HTML is the same).

            var pleaseWaitDiv = angular.element(
                '<div class="modal" id="globalPleaseWaitDialog" data-backdrop="static" data-keyboard="false">' +
                '  <div class="modal-dialog">' +
                '    <div class="modal-content">' +
                '      <div class="modal-header">' +
                '         <h1>Processing...</h1>' +
                '      </div>' +
                '      <div class="modal-body" id="globallPleaseWaitDialogBody">' +
                '         <div class="progress progress-striped active">' +
                '           <div class="progress-bar" role="progressbar" aria-valuenow="100" aria-valuemin="0" aria-valuemax="100" style="width: 100%">' +
                '           </div>' +
                '         </div>' +
                '        <div class="progress-bar progress-striped active"><div class="bar" style="width: 100%;"></div></div>' +
                '      </div>' +
                '    </div>' +
                '  </div>' +
                '</div>'
                );

This is all that I am going to mention about please wait.  Now, let’s get to generic dialog box.  I am going to define an API for it first.  It is pretty simple, I will allow developers to send in text to display and array of buttons to show.  If no buttons are passed in, I will just show Close button to dismiss the dialog.  Each button will have a label and an optional method to invoke when button is clicked.  If no method is passed in, the button will just dismiss the dialog.  Let’s take a look at the div first.

            var messageDiv = angular.element(
                '<div class="modal" id="globalMessageDialog" tabindex="-1" role="dialog" data-backdrop="static" data-keyboard="true">' +
                '  <div class="modal-dialog">' +
                '    <div class="modal-content">' +
                '      <div class="modal-header">' +
                '        <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>' +
                '        <h4 class="modal-title"></h4>' +
                '      </div>' +
                '      <div class="modal-body">' +
                '      </div>' +
                '      <div class="modal-footer">' +
                '       <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>' +
                '      </div>' +
                '    </div>' +
                '  </div>' +
                '</div>'
                );

Here is how my API will looks.

    export interface IUtilities {
        showPleaseWait: () => void;
        hidePleaseWait: () => void;
        showMessage: (content: string, buttons?: IButtonForMessage[]) => void;
    }

    export interface IButtonForMessage {
        mehtod?: () => void;
        label: string;
    }

As you can see, it matches precisely to my goals above.  Based on showMessage method parameters, I am going to adjust the messageDiv.  If a button is passed in, I am going to suppress the ‘x’ to close the dialog.  I am going to inject the buttons into the footer, replacing the close button.  When a button is clicked, I am going to dismiss the dialog first, then invoke the method passed in for that button.  So, with that, here is my entire TypeScript utilities class.

/// <reference path="../../home/interfaces.ts" />
module app.core.services {


    export interface IUtilities {
        showPleaseWait: () => void;
        hidePleaseWait: () => void;
        showMessage: (content: string, buttons?: IButtonForMessage[]) => void;
    }

    export interface IButtonForMessage {
        mehtod?: () => void;
        label: string;
    }

    class Utilities implements IUtilities {
        showPleaseWait: () => void;
        hidePleaseWait: () => void;
        showMessage: (content: string) => void;

        constructor(private $window: ng.IWindowService, private globalsService: interfaces.IGLobals) {
            var that = this;
            var pleaseWaitDiv = angular.element(
                '<div class="modal" id="globalPleaseWaitDialog" data-backdrop="static" data-keyboard="false">' +
                '  <div class="modal-dialog">' +
                '    <div class="modal-content">' +
                '      <div class="modal-header">' +
                '         <h1>Processing...</h1>' +
                '      </div>' +
                '      <div class="modal-body" id="globallPleaseWaitDialogBody">' +
                '         <div class="progress progress-striped active">' +
                '           <div class="progress-bar" role="progressbar" aria-valuenow="100" aria-valuemin="0" aria-valuemax="100" style="width: 100%">' +
                '           </div>' +
                '         </div>' +
                '        <div class="progress-bar progress-striped active"><div class="bar" style="width: 100%;"></div></div>' +
                '      </div>' +
                '    </div>' +
                '  </div>' +
                '</div>'
                );

            var messageDiv = angular.element(
                '<div class="modal" id="globalMessageDialog" tabindex="-1" role="dialog" data-backdrop="static" data-keyboard="true">' +
                '  <div class="modal-dialog">' +
                '    <div class="modal-content">' +
                '      <div class="modal-header">' +
                '        <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>' +
                '        <h4 class="modal-title"></h4>' +
                '      </div>' +
                '      <div class="modal-body">' +
                '      </div>' +
                '      <div class="modal-footer">' +
                '       <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>' +
                '      </div>' +
                '    </div>' +
                '  </div>' +
                '</div>'
                );


            var resize = function (event: JQueryEventObject) {
                var dialog = angular.element('#' + event.data.name + ' .modal-dialog');
                dialog.css('margin-top', (angular.element(that.$window).height() - dialog.height()) / 2 - parseInt(dialog.css('padding-top')));
            };

            var animate = function (event: JQueryEventObject) {
                var dialog = angular.element('#' + event.data.name + ' .modal-dialog');
                dialog.css('margin-top', 0);
                dialog.animate({ 'margin-top': (angular.element(that.$window).height() - dialog.height()) / 2 - parseInt(dialog.css('padding-top')) }, 'slow');
                pleaseWaitDiv.off('shown.bs.modal', animate);

            };

            this.showPleaseWait = function () {
                angular.element($window).on('resize', { name: 'globalPleaseWaitDialog' }, resize);
                pleaseWaitDiv.on('shown.bs.modal', { name: 'globalPleaseWaitDialog' }, animate);
                pleaseWaitDiv.modal();
            };

            this.hidePleaseWait = function () {
                pleaseWaitDiv.modal('hide');
                angular.element($window).off('resize', resize);
            };


            this.showMessage = function (content: string, buttons?: IButtonForMessage[]) {
                angular.element($window).on('resize', { name: 'globalMessageDialog' }, resize);
                
                messageDiv.find('.modal-body').text(content);
                messageDiv.on('shown.bs.modal', { name: 'globalMessageDialog' }, animate);
                if (buttons) {
                    messageDiv.find('.modal-header').children().remove('button');
                    var footer = messageDiv.find('.modal-footer');
                    footer.empty();
                    angular.forEach(buttons, function(button: IButtonForMessage) {
                        var newButton = angular.element('<button type="button" class="btn"></button>');
                        newButton.text(button.label);
                        if (button.mehtod) {
                            newButton.click(function() {
                                messageDiv.modal('hide');
                                button.mehtod();
                            });    
                        } else {
                            newButton.click(function () {
                                messageDiv.modal('hide');
                            }); 
                        }
                        footer.append(newButton);
                    });
                    
                } else {
                    messageDiv.find('.modal-header').html('<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button><h4 class="modal-title"></h4>');
                    messageDiv.find('.modal-footer').html('<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>');
                }
                messageDiv.find('.modal-title').text(globalsService.applicatioName);
                messageDiv.modal();
            };
        }
    }

    angular.module('app.core.services.utilities', ['app.globalsModule'])
        .factory('utilities', ['$window', 'globalsService', function ($window: ng.IWindowService, globalsService: interfaces.IGLobals) {
            return new Utilities($window, globalsService);
        }]);
}

As you can see in showMessage method above, I am implementing my goals – replacing data in all three main areas of the div – footer, header and body.  My buttons are put the footer, and message text into the body.  The reason I have to adjust on every call is because my Utilities class is an Angular service, hence there is only one instance of it for entire application.  So, I have to clean up the div from previous call before adding data of current method call.  Otherwise, I will continue adding buttons endlessly.  The advantage of single instance of course is low memory consumption for both my class and html.  I am using a lot of Bootstrap button CSS classes.  One extension of this code would be to allow developers to pass in classes to apply to additional buttons, but I am skipping this for now.  You also see Angular module registration for Utilities service.  I have been working with Angular for about 4 months now, and I really, really like working with this framework.

You can download entire project here.  Just a side note, I am continuing to build on the same sample demo project.  I am planning to release it when it is done to demonstrate some of the key concepts of developing apps with Angular, Bootstrap, TypeScript and ASP.NET MVC.  More to come, so stay tuned.

Thanks.

2 Comments

Leave a Reply

Your email address will not be published. Required fields are marked *