Modal Dialogs - Programming Chrome Apps (2014)

Programming Chrome Apps

Appendix A. Modal Dialogs

As is pointed out in Chapter 1, you can’t call the usual JavaScript alert and confirm dialogs in a Chrome App (they’re undefined). But, with the new HTML5 <dialog> tag, it’s easy to build modal dialogs using only standard HTML, CSS, and JavaScript.

Before Chrome 37 introduced the <dialog> tag, the way to implement a dialog was to define a <div> element in HTML that completely covered the window, both disabling all user-interface components in the window (making the dialog modal) and partially obscuring the window, making it look disabled. Then, the dialog itself was shown in front with any user-interface components it needed. Now, this complexity is unnecessary. Figure A-1 shows the example app we’ll use in this appendix.

The app’s initial window with two buttons

Figure A-1. The app’s initial window with two buttons

Clicking the button on the left displays an alert dialog, as illustrated in Figure A-2.

An alert dialog

Figure A-2. An alert dialog

Clicking anywhere outside the dialog does nothing. Clicking the OK button dismisses the dialog and restores the original screen. Then, clicking the right button at the top pops up the confirm dialog in Figure A-3.

Clicking Yes shows the alert in Figure A-4. Clicking No shows an analogous alert.

All you need to do to implement a dialog is to add a <dialog> tag to the body:

<dialog id="dlg-dialog">

<!-- dialog UI goes here -->

</dialog>

You show the dialog modally by using the JavaScript showModal method and dismiss it with close.

By default, a dialog is pretty plain. The more attractive dialogs shown in the examples use this CSS:

#dlg-dialog {

border: 1px solid rgba(0, 0, 0, 0.3);

border-radius: 6px;

box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3);

}

The confirm dialog

Figure A-3. The confirm dialog

Alert dialog after clicking the Yes button

Figure A-4. Alert dialog after clicking the Yes button

The method that’s called to show the dialog adds additional HTML within the <dialog> element, which is what displayed the messages and buttons in the previous examples. We’re going to see that soon.

It’s most convenient if all of the code for these dialogs, including HTML, CSS, and JavaScript, is placed in a single JavaScript file. That way, all you have to do is add a <script> tag to your HTML, without adding anything else and, especially, without actually adding HTML for the dialog. This is done by creating HTML and CSS for the dialogs dynamically in JavaScript.

The JavaScript for the dialog should also be a module so that any internal functions it uses are hidden. (See Modules and Module Loading.) For dialogs, the JavaScript for the module is in a file named Dialogs.js and looks like this:

var Dialogs = (function () {

return {

alert: function (msg) {

// put up the alert dialog

},

confirm: function(msg, btnYes, btnNo, actionYes, actionNo) {

// put up the confirm dialog

}

}

function internal_function1() {

// ...

}

function internal_function2() {

// ...

}

})();

The alert method takes a message argument, and the confirm method takes that and four additional arguments for the names of the buttons and the callback functions if either of those buttons is clicked. The code that follows is for the example program and shows you how to use those methods. First is the index.html file, referenced when the background.js code created the window, just as in all the previous Chrome App examples:

<!DOCTYPE html>

<html lang="en">

<head>

<meta charset="utf-8" />

<title>Dialog Example</title>

<script src="Dialogs.js"></script>

<script src="DialogExample.js"></script>

</head>

<body>

<button id="alert">Show Alert Dialog...</button>

<button id="confirm">Show Confirm Dialog...</button>

</body>

</html>

Observe the <script> reference to Dialogs.js, which contains the Dialogs module. We’ll get to that shortly.

Here’s the example program in DialogExample.js:

window.onload = function () {

document.querySelector("#alert").addEventListener("click",

function () {

Dialogs.alert("This is the alert.");

}

);

document.querySelector("#confirm").addEventListener("click",

function () {

Dialogs.confirm("Click one of the buttons:", "Yes", "No",

function () {

Dialogs.alert("You clicked Yes");

},

function () {

Dialogs.alert("You clicked No");

}

);

}

);

};

Of interest here are the calls to the two methods, Dialogs.alert and Dialogs.confirm, and the two callbacks for the latter, which also call Dialogs.alert.

Now, going back to the module in Dialogs.js, it has an internal function that adds the needed HTML and CSS to the body:

function setup() {

if (!document.querySelector("#dlg-dialog")) {

dlg = document.createElement("dialog");

dlg.id = 'dlg-dialog';

document.body.appendChild(dlg);

var css = document.createElement("style");

css.type = "text/css";

css.innerHTML =

"#dlg-dialog {" +

" border: 1px solid rgba(0, 0, 0, 0.3);" +

" border-radius: 6px;" +

" box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3);" +

"}";

document.body.appendChild(css);

}

}

The code is a little hard to follow, but if you study it, you’ll see that all it does is add the HTML and CSS we saw previously to the document. It first tests to see if it’s already added, so it’s set up only once.

Here is the method for alert, which was sketched in the preceding module example:

alert: function (msg) {

dialog("<p>" + msg + "</p><button id='dlg-close'>OK</button>",

[

{

id: 'dlg-close'

}

]

);

},

This method calls the internal dialog function with two arguments:

§ HTML for the dialog, which in this case is the message with a button underneath it

§ An array with an element for each button, each of which consists of the id for that button and an action, the default being to simply close the dialog

The method for confirm is a little more involved because it has two buttons and actions:

confirm: function(msg, btnYes, btnNo, actionYes, actionNo) {

dialog(

"<p>" + msg + "</p><button id='dlg-no'>" + btnNo + "</button>" +

"<button id='dlg-yes'>" + btnYes + "</button>",

[

{

id: "dlg-no",

action: actionNo

},

{

id: "dlg-yes",

action: actionYes

}

]

);

}

Here’s the internal dialog function that does most of the work:

function dialog(html, actions) {

setup();

dlg.innerHTML = html;

dlg.showModal();

var funcs = [];

for (var i = 0; i < actions.length; i++) {

funcs[i] = (function(index) {

return function() { // index bound here instead to function dialog

dlg.close();

if (actions[index].action)

actions[index].action();

}

})(i);

document.getElementById(actions[i].id).addEventListener("click", funcs[i]);

}

}

The first three lines are straightforward: they call setup to add the HTML and CSS if they’re not already added, set the supplied HTML (the first argument to dialog) as the inner HTML for the dialog, and then show it modally.

The tricky part is setting up handlers for the buttons, each handler stored as an element of the funcs array. The obvious way to do that is like this:

for (var i = 0; i < actions.length; i++) {

document.getElementById(actions[i].id).addEventListener("click",

function () {

dlg.close();

if (actions[i].action) // i is bound to function dialog

actions[i].action();

}

);

}

But, this won’t work because (as the comment indicates) the variable i is bound to the dialog function, not to the event-handling function. Its value when the event handler is called will be whatever the for loop set it to when it terminated (perhaps 3). To fix this problem, you must call a function to set each element of func to the event-handler function, which gets the index bound to that intermediate function, unique to each button, instead of to the outer dialog function. That’s what’s done in the dialog function. (This is a trick you’ll want to use whenever you’re having this sort of function-closure problem.)

After dialog is implemented, you can add any number of customized methods in addition to alert and confirm.