Showing Message Boxes and Modals

####

Key Points

Message Boxes

Showing a message box in Durandal is easy. Simply configure the plugin, then require the app module and call showMessage.

Let's first see how to install the plugin. In your main.js file, before calling app.start() you will need to make a call to configurePlugins(...). Here's what your code might look like:

define(['durandal/app'],  function (app) {
    app.configurePlugins({
        dialog: true
    });

    app.start().then(function () {
        app.setRoot('shell');
    });
});

Once the plugin is installed, you can show a message like this:

var app = require('durandal/app');
...
app.showMessage('This is a message.');

This will display a message box with the default title and the message "This is a message." It will also display an "Ok" button. By default, your message box's title is set to your app.title. If you haven't set the title, then it defaults to "Application". You can easily add a custom title by providing the second parameter. Here's an example:

app.showMessage('This is a message.', 'Demo');

This message box is the same as the first, but with "Demo" displayed in the header. Now, suppose you want more than just an "Ok" button. The third parameter to this function specifies an array of options to display to the user. Here's what that looks like:

var screen = {
  ...
  canDeactivate:function(){
    return app.showMessage('You have unsaved data. Are you sure you want to close?', 'Unsaved Data', ['Yes', 'No']);
  }
  ...
};

This message box will have two buttons "Yes" and "No". The yes button, which is first in the list, will be configured as the default action. Notice also that a call to showMessage has a return value. This return value is actually a promise of the user's response. It will be resolved when they select an option and the message box is closed.

Customizing Message Boxes

As of Durandal 2.1, message boxes can be customized in a number of ways. The app.showMessage method has the following signature:

app.showMessage = function(message, title, options, autoclose, settings)

Options - Message Box Buttons

Options, that is, message box buttons, can be specified in two ways:

  1. An array of strings: This will generate buttons with the same text as the strings provided. Also, the returned value, when the button is clicked, will be the provided string. Example:

    ["Yes", "No"]
    
  2. An array of objects: Each object must have two properties: text and value. text specifies the text of the button and value the value that is returned, when the button is clicked. text must be a string, value can be any JavaScript object. This way of specifying buttons is useful, for example, for localizing button texts. Example:

    [ { text: "Ja", value: "Yes" }, { text: "Nein", value: "No" }]
    

Autoclose

You can pass a fourth parameter, a boolean autoclose, to app.showMessage and dialog.showMessage. It specifies whether the message box will close on a background click. The default is false.

Settings

The fifth parameter of app.showMessage and dialog.showMessage is settings, which specifies additional behaviours for the message box. It is a JavaScript object, for which you can specify the following properties:

Button Classes

You can customize the classes that are rendered to buttons with buttonClass, primaryButtonClass, and secondaryButtonClass. buttonClass will be rendered to all buttons, primaryButtonClass to the first button only, and the secondaryButtonClass to all other buttons. This allows you to change the button positions by float:left and float:right as you want.

Dialog Class and Styles

The class and style settings allow you to customize the MessageBox dialog. The class setting allows you to define the class of the message box in question. It defaults to "messageBox". For example:

app.showMessage("Message", "Title", ["Close"], true, { "class": "messageBox2" });

This will change the class of the message box from messageBox to messageBox2.

The style setting is forwarded to Knockout's style-binding. So, you can use it as follows:

app.showMessage("Message", "Title", ["Close"], true, { style:  { width: "600px", backgroundColor: "red" } });

That is, the style setting can take an object of styles and their settings. This allows you to customize the message box even more.

Default Settings

Often it is handy to set the default settings that apply to all message boxes instead of specifying them to each message box separately. This you can do as follows:

dialog.MessageBox.setDefaults({ buttonClass: "iconbutton", primaryButtonClass: "float-right", secondaryButtonClass: "cancelbutton float-left" });

dialog.MessageBox.setDefaults specifies only the settings that you define in the object passed to the method, leaving all other settings intact. You can run this method in your application startup.

Custom Dialogs

It turns out that message boxes are just an implementation of a custom modal dialog. With Durandal, you can show any UI as a dialog, using the same techniques as you aready know. Just create a view and a view model. Let's see how to do it by looking at how the built-in message box is implemented. First, let's see the implementation of showMessage:

showMessage: function (message, title, options, autoclose, settings) {
    return dialog.showMessage(message, title, options, autoclose, settings);
}

This shows the typical technique for displaying a custom dialog. All you have to do is pass an object to dialog.show and under the covers it will use the composition system to locate its view and bind the two together. Then, it will display it as a dialog.

Let's see part of the implementation of MessageBox and its view.

messageBox.js

var MessageBox = function (message, title, options, autoclose, settings) {
    this.message = message;
    this.title = title || MessageBox.defaultTitle;
    this.options = options || MessageBox.defaultOptions;
    this.autoclose = autoclose || false;
    this.settings = $.extend({}, MessageBox.defaultSettings, settings);
};

MessageBox.prototype.selectOption = function (dialogResult) {
    dialog.close(this, dialogResult);
};

messageBox.html

<div data-view="plugins/messageBox" data-bind="css: getClass(), style: getStyle()">,
    <div class="modal-header">
        <h3 data-bind="html: title"></h3>
    </div>
    <div class="modal-body">
        <p class="message" data-bind="html: message"></p>
    </div>
    <div class="modal-footer">
        <!-- ko foreach: options -->
        <button data-bind="click: function () { $parent.selectOption($parent.getButtonValue($data)); }, text: $parent.getButtonText($data), css: $parent.getButtonClass($index)"></button>
        <!-- /ko -->
        <div style="clear:both;"></div>
    </div>
</div>

As you can see, MessageBox is just a normal prototype-based class. It stores its parameters and provides a function to select a particular option. The view is located by the composition system and bound to it before displaying it in a modal style. The view uses a standard set of Knockout bindings along with some css classes. There's one important detail to notice. Have a look at the selectOption function of the modal implementation. It uses the dialog module's close function to dismiss the dialog. The interesting thing about it is that you can pass it a value to return to its caller. When the caller invokes show a promise is returned which, as mentioned above, resolves when the dialog closes and will carry with it whatever the dialog passes to close. Here's how a client might obtain that value:

var app = require('durandal/app');
...
app.showMessage('This is a message.').then(function(dialogResult){
  //do something with the dialog result here
});

That's all there is to creating a custom dialog. It's built like any other UI, by constructing a module and a view. Then, just call app.showDialog and you are in business.

Dialog Contexts

All dialogs are shown in a particular visual context. This is a customization point that allows developers to leverage the dialog module's composition and activation infrastructure while controlling the visualization of the dialog.

The Default Dialog Context

The default context has the following basic behavior:

  1. Displays a blockout over your app. You can change it's opacity by using getContext and changing the context's blockoutOpacity property.
  2. Displays your dialog's view centered on the screen.
  3. Displays your dialog's view by toggling its opacity from 0 to 1 when ready. This allows for a css 3 transition animation. (Default styles are provided by bootstrap and the durandal.css under source.)
  4. When displaying your view, if it's root html element has a class of autoclose then the context will ensure that clicking outside of your dialog automatically causes it to close.
  5. When displaying your view, the context will look for any child elements with a class of autofocus and focus them.
  6. When removing your view, the same css 3 opacity animation is applied. If you wish to change the timing of this, after you change the animation, you should alter the context's removeDelay. This is a delay in milliseconds that waits for the animation to complete before removing the dom nodes from the tree.
  7. While the dialog is visible, scroll bars are hidden on the body/html.

Note: The default dialog context has some required css for positioning which can be found in the durandal.css file. It assumes that the target browser supports position: fixed. If your target browsers do not support this, you should replace the default dialog context with a custom implementation.

Custom Dialog Contexts

You an use the addContext API to add a new dialog context to the system. The context should have the following functions defined:

Note: Whenever you call addContext the dialog module will add a helper method to itself to facillitate showing dialogs in that context. For example, if your create and add a context called 'bubble' for showing bubble popups, you could show these popups in two different ways: dialog.show(viewModel, activationData, 'bubble') or dialog.showBubble(viewModel, activationData)

Repositioning Dialogs After Their Contents Have Been Changed

On the default dialog context, there is a method to reposition a dialog after its contents have been changed (called reposition). You can call it as follows:

dialog.getContext().reposition(view);

or

var theDialog = dialog.getDialog(context.model);
theDialog.context.reposition(view);

where view is the view that needs to be repositioned. You need to call this method, for example, after you have loaded some content via AJAX to the dialog.

Setting Full Screen Dialog Margins

The default margins for a dialog that fills the whole screen is 5 pixels on each side. If you want to alter these margins, you can do it as follows, preferably in your main.js:

dialog.getContext().minYMargin = 35;
dialog.getContext().minXMargin = 50;

This will set the top and bottom margins to 35 px each and the left and right margins to 50 px each.