In this article, I have updated the modal dialog compatible with Angular 1.5x and TypeScript, and also provided an example to open a dialog in the component controller class.
Update Notice: The updated NgExDialog has been added here to work on web applications that use the Angular 1.5x components and TypeScript. Please see more descriptions in the new section inserted to the end of the article. For developers who are interested in the Angular 2 version with the same functionality and features, please see the article and sample code for the Ng2ExDialog.
Introduction
I previously created a full-featured JQuery dialog plugin, the jqsDialog, for building web pages. Lately, I needed the same kind of the modal dialog when developing website applications in AngularJS. Although many ready-use AngularJS modal dialog tools are available from the developer’s communities and other sources, none could be found as with the advanced features as the jqsDialog
. I thus again created my own AngularJS modal dialog library, named as NgExDialog
, to match all features delivered by the jqsDialog
except for the non-modal option, which has very little practical significance, and the progress bar, as most website applications use an AJAX loader display instead.
The NgExDialog
has these features:
- Easy to use with standardized and simplified calling code
- Flexible and customizable for both common messaging and data display purposes
- Dialogs can be opened to any level deep and closed or kept open individually, closed with immediate parent together, or closed for all
- Configurable for all options, such as draggable, animation, icon, gray-background, close-by-click-outside, cancel confirmation, etc.
- Themes and styles can be set for each component, such as main dialog, header, title, icon, message body, message text, footer, and buttons
- Distribution with single JavaScript file and linked CSS file, plus two image files if the icon display is enabled
- The dialog has only dependency on the angular.js and bootstrap.css. No other library or file reference is needed.
Based on these outstanding features, the internal code of the NgExDialog
is somewhat complex. This article will not attempt to discuss the coding details inside the ngExDialog.js and ngExDialog.css files, but rather focus on how to use the tool in web applications and also address some major issues with solutions related to the use cases. If any audience is interested in NgExDialog
internal code and structures, feel free to look into the details from the downloaded source. There are always comments for any code block or line if it’s not self-explanatory.
Dialog Access Scenarios and Syntax
The NgExDialog
is built as an AngularJS service provider. In the sample, the code for opening dialogs are all in the controller.js and no top-level directive is available. Opening a dialog with a directive from AngularJS views is less practical than the code from AngularJS controllers in the real world since the dialog acts as a conditional and dynamic pop-up along a course of the application workflow.
Opening dialogs can follow these scenarios (with the NgExDialog
already injected into the application module and the exDialog
as called provider name):
-
Using a simple line of parameters for a message or confirm dialog if only default settings are needed or only the body text, title, or icon are specified.
Syntax:
[custom-object] = exDialog.openMessage($scope, "message-body", ["title"], ["icon"]);
[premise-object] = exDialog.openConfirm($scope, "message-body", ["title"], ["icon"]);
-
Using the parameter object with needed properties for a message or confirm dialog if requiring any non-default setting other than the body text, tile, or icon.
Syntax:
[custom-object] = exDialog.openMessage($scope, parameter-object);
[premise-object] = exDialog.openConfirm($scope, parameter-object);
-
Always passing the parameter object with needed properties for any custom or data loading dialog.
Syntax:
[custom-object] = exDialog.openPrime($scope, parameter-object);
The parameter object accepts these properties as listed below. The same can be found above the openMessage()
method in the ngExDialog.js file.
@params {Object}:
//These for message and confirm types with built-in template only.
- title {String} - dialog title in header, default to "information" or configured
- icon {String} - values are 'info', 'warning', 'question', 'error',
default to "info" or configured
- message {String} - body message
- closeButtonLabel {String} - close button label, default to "OK" for alert
- and "No" for confirm, or configured
- actionButtonLabel {String} - Action button label, default to "Yes" for confirm,
- or configured
- closeAllDialogs {Boolean} - close all dialogs including parents
- keepOpenForAction {Boolean} - keep previous confirmation dialog open when clicking
- action button, default to undefined, or configured
- keepOpenForClose {Boolean} - keep previous confirmation dialog open when clicking
- close button, default to undefined, or configured
- dialogAddClass {String}
- headerAddClass {String}
- titleAddClass {String}
- bodyAddClass {String}
- messageAddClass {String}
- footerAddClass {String}
- actionButtonAddClass {String}
- closeButtonAddClass {String}
//These for all types including custom template.
- scope {Object} - source scope
- template {String} - id for ng-template script, url for file,
- or plain string containing HTML text. Required for openPrime()
- controller {String} - required if the custom template needs it.
- width (String} - dialog width, configured
- closeByXButton {Boolean} - show x close button, default true, or configured
- closeByEscKey {Boolean} - default true, or configured
- closeByClickOutside {Boolean} - default true, or configured
- beforeCloseCallback {String|Function} - user supplied function name/function
- called before closing dialog (if set)
- grayBackground {Boolean} - default true, or configured
- cacheTemplate {Boolean} - default true, or configured
- draggable {Boolean} - default true, or configured
- animation {Boolean} - default true, or configured
Basic Use Case Examples
For sample demonstrations, you can downloaded source and add folders and files into any website project as long as it supports the HTML and JavaScript. All features of the ngExdialog
should work well with the latest versions of Internet Explorer, Google Chrome, and Firefox. It's not guaranteed that other browser types and versions can do all the same.
Running the index.html will start the page showing the links for opening dialogs. You can even test any other cases with your own code to call the NgExDialog
service.
-
Opening an information message dialog with required body text only:
exDialog.openMessage($scope, "This is called from a simple line of parameters.");
-
Opening a warning message dialog with required message text only:
exDialog.openMessage($scope, "This is called from a simple line of parameters.",
"Warning", "warning");
-
Opening a confirmation dialog with required body text only:
exDialog.openConfirm($scope, "Would you like to close the dialog
and open another one?").then(function (value) {
exDialog.openMessage($scope, "This is another dialog.");
});
-
Opening a message dialog with the animation and draggable disabled (Note that the animation and draggable features are enabled for all dialogs by default):
exDialog.openMessage({
scope: $scope,
message: "Animation and drag-move disabled.",
animation: false,
draggable: false
});
The dialog displayed with all default title, icon, and button but there is no animation and draggable effects. You can see the result by clicking the Dialog without Animation and Dragging link on the demo page.
-
Opening a custom data form dialog:
exDialog.openPrime({
scope: $scope,
template: 'Pages/_Product.html',
controller: 'productController',
width: '450px'
});
In this case, the NgExDialog
provides the main frame features of the dialog. All contents and page-dialog communication processes are defined in the specified template and controller, including the action and close buttons, and all content styles. This will be more flexible for developers to design and implement the data form and its operations.
Dialog Display Templates
The built-in template in the NgExDialog
for message and confirmation types of dialogs usually meets the needs of most common uses. The themes and styles can even be customized at the single component level. In case you need to modify it or add new components into it, you can make changes in the included commonDialog_0.html file and then use it as default template by switching the configuration item in the app.js file.
angular.module('smApp', ['smApp.controllers', 'smApp.AppServices', 'ngExDialog', function () {
}])
.config(['exDialogProvider', function (exDialogProvider) {
exDialogProvider.setDefaults({
template: 'ngExDialog/commonDialog_0.html',
. . .
});
}]);
You can also make changes in the commonDialogController
inside the ngExDialog.js file if any added component needs code supports from its controller.
A particular custom template and its controller should be created for any types other than the common message or confirmation dialogs, such as the data form dialog (see the _Product.html template and productController
in the controller.js from the sample source). In this scenario, the dialog uses the core features of the NgExDialog
to interact with the environment. The custom template is responsible for the content of the visible dialog area. Thus, any data process, communication between the template and its controller, and look-and-feel of the dialog will be handled by your own code.
There are several forms of templates you can choose:
-
id
attribute name in the ng-template
script code:
<script type="text/ng-template" id="customDialogTemplate">
<!--HTML code here-->
. . .
</script>
-
URL path of the template HTML file, such as "/Pages/_Product.html".
-
HTML text as a pure string beginning with the open tag symbol "<
".
The template loader will parse the input values and automatically select the correct template form and content. No other indicator or flag is needed. The template will be cached before use if no cache for the same template exists unless you change this default behavior by setting the cacheTemplate
input parameter object property to false
.
Closing or Keeping Open Dialogs
There is a major structural difference between the NgExDialog
and jqsDialog
when used as multi-level common message and confirmation dialogs. The jqsDialog
mostly re-uses an existing object instance and dynamically change the content of the object, such as body text, title, icon, and/or buttons, for a child dialog. The NgExDialog
, however, always uses a new object instance to open a child dialog. By default, it firstly closes the parent dialog and then opens its child. Unlike most other AngularJs modal dialog tools, the NgExDialog
provides options to keep any level of parent dialogs open on the background when a child dialog is shown. There are at least these benefits when enabling this feature:
- Some dependent processes need co-existence of both parent and child dialogs, even for non-data-access dialogs.
- When needed, users can see all dialogs loaded for the workflow.
- The shuffling and flicking visual effects due to dialog transitions can be avoided.
The option can be enabled using the input parameter object properties for the dialog that will be kept open:
exDialog.openConfirm({
scope: $scope,
. . .,
keepOpenForAction: true,
keepOpenForClose: true
});
For a confirmation type dialog, the keepOpenForAction
is for keeping the dialog open when clicking the action button, such as Yes, OK, Go, or Continue, and the keepOpenForClose
is for clicking the close button, such as No or Cancel. For a message type dialog with only one OK, Go, or Continue button, only keepOpenForClose
is available.
In most situations, commands of also closing immediate parent or closing all dialogs are needed from a child dialog when using the options to keep parent dialogs open.
If also closing the immediate parent from the code for a child dialog:
exDialog.openMessage({
scope: $scope,
. . .,
closeImmediateParent: true
});
If closing all dialogs:
exDialog.openMessage({
scope: $scope,
. . .,
closeAllDialogs: true
});
The existing parent dialog is always behind the newly opened child dialog. The parent dialog may not be seen at all if its size is smaller than the overlapped child dialog. Since the NgExDialog
has the draggable feature (described later), the child dialog can be moved aside to view the underlying parent dialog.
Running Tasks when Closing Dialogs
For a dialog, commands to run tasks are usually initiated from the action button. The application workflow may sometimes need to run additional tasks when closing a dialog, such as a cancel warning, further confirmation, or redirecting to other pages. Three options are available for running tasks when a dialog is closed.
-
Using custom callback function for any base screen of common message or confirmation dialog. You can specify a callback function for the input parameter object property beforeCloseCallback
:
exDialog.openConfirm({
scope: $scope,
actionButtonLabel: "Continue",
closeButtonLabel: "Cancel",
message: "What next step would you like to take?",
beforeCloseCallback: function (value) {
var rtnPremise = exDialog.openConfirm({
scope: $scope,
message: "Do you really want to cancel it?"
});
return rtnPremise;
}
});
With responding to the cancel confirmation, the workflow will be cancelled and all pop-up screens are closed when clicking the Yes button or it will return to the previous base screen that keeps everything as before when clicking the No button.
-
Using the close premise object for the base screen of a confirmation dialog.
exDialog.openConfirm($scope, "Would you like to open a second dialog?").
then(function (value) {
exDialog.openMessage($scope, "This is the second dialog.");
}, function (reason) {
exDialog.openMessage($scope, "The dialog has been closed.");
});
By default, the task running to the response of closing the dialog occurs after the dialog has been closed. Thus, this scenario is best used for a workflow that is not returned back to the base dialog screen. The below screenshot shows the transition moment of closing the cancel confirmation dialog and opening the final notification message dialog:
-
Opening a confirmation dialog directly from the close button event for any base dialog with a custom template. This approach is pretty straightforward since the close button and its attributes are specified within the custom template. Here is the code for cancel warning and confirmation in the data form dialog example.
exDialog.openConfirm({
scope: $scope,
title: "Cancel Warning",
icon: "warning",
message: "Do you really want to cancel the data editing?"
}).then(function (value) {
exDialog.openMessage({
scope: $scope,
title: "Notification",
message: "The editing has been cancelled."
});
}, function (reason) {
exDialog.openMessage({
scope: $scope,
title: "Notification",
message: "The editing will continue."
});
});
The screenshot shows the result when clicking No button on the Cancel Warning dialog:
Draggable Dialogs
A draggable dialog allows user to watch any part of the underlying page content and hence is a user-friendly add-on. The NgExDialog
is fully draggable and well adaptable to the screen resizing. The draggable option is set by default. You can turn off this feature at the application configuration level or disable it for any individual dialog as the example shown before.
Some particular comments are worth mentioning for using the draggable feature.
-
When enabling the draggable for any dialog with a custom template containing input type elements, you need to specify additional ng-focus
and ng-blur
attributes for each input element as in the _Product.html example like this:
<input type="text" class="form-control" data-ng-model="model.Product.ProductName"
ng-focus="setDrag(true)" ng-blur="setDrag(false)" />
This is because the draggable directive in the NgExDialog
doesn’t call the preventDefault()
function of the mousedown
event which, if called, disables the input fields on the dialog. But with HTML default setting, when trying to highlight the text in the input field with the mouse, the entire dialog will be moving causing the normal text highlight functionality to fail. Thus, a flag is set inside the NgExDialog
that receives boolean values from those input fields to disable and re-enable the dragging action when the mouse point is on and off any input field, respectively.
On below screenshot, the dialog cannot be dragged and moved when the input field is getting focused:
-
When dragged and moved, the NgExDialog
also disables possible selection of display text on the dialog and underlying page. Occasionally, selections of the display text may still occur especially if the dialog is moved to, or out of, window edges. This is due mainly to the browser compatibility issue or the browser doesn't fully support this line of JavaScript code used inside the draggable directive:
window.getSelection().removeAllRanges();
-
Resizing the window will always re-center the dialog within the window if the dialog has not been dragged since it opens. If the dialog is dragged and then the window is resized, the horizontal position of the dialog will be re-adjusted normally. The vertical location of the dialog, however, will be fixed on the points where the previous dragging ends. It’s not a bug. Such a behavior is intentionally implemented as a workaround to resolve the issue related to the vertical centering of the dialog. If resizing the window vertically to make the window's height smaller, it could result in part or all of the dialog out of the window at the dialog fixed position. Users can re-adjust the dialog position before resizing the window again for this case.
Customizing Styles for Built-in Template
Additional CSS classes can be specified for components of the common message or confirmation dialog with the build-in template. For example, the dialog needs a single line border when displayed on the screen without the gray background. We can then add the dialogAddClass
property into the input parameter object and specify the border-to-dialog
CSS class in the dialog level.
exDialog.openMessage({
scope: $scope,
title: "No Icon, No Gray",
icon: "none",
message: "This is called by passing a parameter object",
grayBackground: false,
dialogAddClass: 'border-to-dialog'
});
It’s also very easy to make changes in header and footer styles for a particular dialog by adding the properties, headerAddClass
and footerAddClass
, to the parameter object and then creating corresponding CSS classes:
exDialog.openMessage({
scope: $scope,
title: "Look Different",
icon: "none",
message: "Show header and footer in other styles.",
headerAddClass: 'my-dialog-header',
footerAddClass: 'my-dialog-footer'
});
The full list of available input parameter object properties for adding dialog component CSS classes are described in the beginning section. Any or all of these properties can also be set as default values from the application level configurations if it’s required to make all dialogs the same look-and-feel across the entire application.
Closing Dialogs with Browser Navigations
On the AngularJS page, any browser redirection to other site will automatically close any open dialog. However, there are some issues related to the browser's back and forward buttons.
Issue #1: Browser back button is enabled when opening a dialog on the page having no history activity. This may be caused by loading the dialog template HTML that has the location URL. This false button-enabling behavior should be avoided although clicking the back or forward button doesn't do anything. The resolution is simply to inject the AngularJS $location
into the controllers that use the dialogs without adding any other code.
.controller('sampleController', ['$scope', '$timeout', 'exDialog',
'$location', function ($scope, $timeout, exDialog, $location) {
}])
Issue #2: Browsing back and forward on a page having any history activity and open dialog. This could keep the current modal dialog still shown over the background after switching to the previously visited page. Below approaches are used to resolve the issue.
- Adding a function, hasOpenDialog() to return a boolean flag for any open dialog in the current scope.
hasOpenDialog: function () {
if (document.querySelector('.dialog-main')) {
return true;
}
else {
return false;
}
}
- In the HTML body controller, the top level controller in the AngularJS SPA application, placing the code in the
$locationChangeStart
event handler. It will close any open dialog when the routing location changes and calling hasOpenDialog()
returns true
.
.controller('bodyController', ['$scope', 'exDialog', '$location',
function ($scope, exDialog, $location) {
$scope.$on('$locationChangeStart', function (event, newUrl, oldUrl) {
if (newUrl != oldUrl && exDialog.hasOpenDialog()) {
exDialog.closeAll();
}
});
}])
The below screenshots illustrate the browser-back operations.
When browsing to the second page and opening a dialog on that page:
Clicking the browser back button:
The dialog is automatically closed and the process returns to the first main page:
Angular 1.5x TypeScript Compatible Updates
Angular 1.x has been considered as obsolete JavaScript framework since Angular 2 becomes stable. However, many existing web applications using the Angular 1.x still need tool or library supports. I would like to make the NgExDialog
compatible to the Angular 1.3x through 1.5x and TypeScript, and also callable from the Angular 1.5x component code with minimal changes, rather than upgrading the NgExDialog
to full TypeScript and component structures. The changes are outlined as follows:
- Converted the Angular JS to TypeScript code for the
NgExDialog
and created the ngExDialog.ts file which works for TypeScript versions 1.8.3 to 2.1.5. - Edited the code for working with the Angular 1.3x to 1.5x. The downloaded source includes the version 1.5.8.
- Checked that all of the functionality and features were not impacted.
The demo sample application works using either Visual Studio 2015 or 2017. The code on the second sample page has been re-written to open a dialog from Angular 1.5x component controller class.
The component node is specified in the secondSample.html:
<div class="container">
<sample-second></sample-second>
</div>
The view content comes to the _sampleSecondTemplate.html:
<div >
<h4 class="panel-indent">Test Browser Navigation Buttons</h4>
<a class="hy-link cursor-pointer" ng-click="vm.openSimpleInfo()">
Open Information Dialog</a>
</div>
All TypeScript classes and interface are in the secondSampleComponent.ts. Here are main code lines of the SecondSampleController
for opening a message dialog (you can see all other details in that file):
class SecondSampleController implements ISecondSampleController {
static $inject = ['exDialog', '$scope']
constructor(private exDialog, private $scope) { }
openSimpleInfo() {
this.exDialog.openMessage(this.$scope, "Open a dialog on second page.");
}
}
Summary
The AngularJS modal dialog, NgExDialog
, presented here is rich of functionality and yet very easy to use. I'm happy to share the tool and sample demo code with the developer's communities. Hope that web developers would like the tool and code. Any feedback will be welcome.
History
- 18th May, 2015
- 24th August, 2015
- Added the resolution for closing dialogs when clicking the browser back or forward button. See the Closing Dialogs with Browser Navigations section for details.
- Source code files have also been updated.
- 28th March, 2017
- Updated
NgExDialog
compatible to Angular 1.5x and TypeScript. A use example is provided in the component structure in the second sample page. - Sample code running with Visual Studio 2015 or 2017 is added to the downloading list