The article with attached source code files for multiple project types describes the advanced features and uses of an Angular modal dialog. Some common and user-reported issues and resolutions are discussed in details.
Introduction
To keep the consistency with the Angular and npm package naming conventions, the modal dialog tool name, apart from the sample application or project names, has been changed from the NgExDialoge
to the ngex-dialog
. The tool and sample demo application have been updated several times with the Angular versions since the code had been rewritten from the AngularJs version. The source code in Angular 11 CLI and with the project types including ASP.NET Core 5.0 is available for downloading. If you need the source code in previous versions of Angular, please see the History section by the end of the article.
The Angular ngex-dialog
has these features:
- Easy to use with standardized and more simplified calling code.
- Flexible and customizable for both basic 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.
- The vertical alignment of the dialog box with custom and dynamically loaded content can be fine-tuned with the top-offset setting.
- Themes and styles can be set for each element, such as main dialog, header, title, icon, message body, message text, footer, and buttons.
- Distribution with both single directory containing all script files for the dialog service as downloaded sources (source-code edition) from this article and a library package installed via npm (library package edition).
The major dependencies of an Angular web application all apply to the ngex-dialog
. Please check the package.json file in the sample project to view those dependent library tools and files. You also need the node.js (recommended version 14.x LTS or above) and Angular CLI (recommended version 11.x or above) installed globally on the local machine. Please check the node.js and Angular CLI documents for details.
Setting Up Sample Application
The downloaded sources contain two types of Visual Studio solutions/projects that include the original TypeScript module and component code files under the ngex-dialog directory (source-code edition) and one type of generic Angular CLI demo project that calls ngex-dialog
library package (npm library package edition). Please pick up the type you would like for the setup on your local machine.
You may check the available versions of the TypeScript for Visual Studio in the C:\Program Files (x86)\Microsoft SDKs\TypeScript folder. Both ASP.NET and Core types of the sample application set the version of TypeScript for Visual Studio to 4.0 in the TypeScriptToolsVersion
node of SM.NgDataCrud.Web.csproj file. If you don't have the version 4.0 installed, download the installation package from the Microsoft site or install the Visual Studio 2019 version 16.8.x which includes the TypeScript 4.0.
NgExDialog_AspNetCore_Cli
-
You need to use the Visual Studio 2019 (version 16.8.x) on the local machine. The .NET Core 5.0 SDK is included in the Visual Studio installation.
-
Download and unzip the source code file to your local work space.
-
Go to physical location of your local work space and double click the npm_install.bat and ng_build.bat (or ng_build_local.bat if not installing the Angular CLI globally) files sequentially under the SM.NgExDialog.Sample\wwwroot\ang-content folder.
NOTE: The ng build
command may need to be executed every time after making any change in the TypeScript/JavaScript code, whereas the execution of npm install
is just needed whenever there is any update with the node module packages. I do not enable the CLI/Webpack hot module replacement (HMR) in this sample application.
-
Open the solution with the Visual Studio 2019, and rebuild the solution with the Visual Studio.
-
Click the IIS Express toolbar command (or press F5) to start the sample application.
NgExDialog_AspNet_Cli
-
Download and unzip the source code file to your local work space.
-
Go to physical location of your local work space and double click the npm_install.bat and ng_build.bat (or ng_build_local.bat if not installing the Angular CLI globally) files sequentially under the SM.NgExDialog.Sample\ang-content folder (also see the same NOTE for setting up the NgExDialog_AspNetCore_Cli project).
-
Open the solution with Visual Studio 2017 or 2019 and rebuild the solution with the Visual Studio.
-
Click the IIS Express toolbar command (or press F5) to start the sample application.
ngex-dialog.demo
-
Download and unzip the source code file to your local work space (any system platform).
-
Go to physical location of your local work space and open the ang-content directory.
-
Execute the command npm install
to add the node_modules into the directory.
-
Run the Angular project application from your system to render the Dialog Demo page.
If you would like to use the ngex-dialog
npm library package in the Visual Studio projects, you can simply replace the ang-content folder in the Visual Studio projects with the ang-content folder from the downloaded ngex-dialog.demo. You can then install or update the npm
packages followed by building the Angular application.
Dialog Access Scenarios and Syntax
The ngex-dialog
is built as an Angular service provider. You can see all demo cases in sections later. For using the ngex-dialog
in your own project, you simply need to do the following (also see the details in the source code files):
- Copy the entire ngex-dialog folder to your development project location or add the ngex-dialog library package into you project via npm installation or updating package.json.
-
Add and update the styles for your modal dialogs with the ex-dialog.css file. See the example in the ang-content/src/assets/css/ex-dialog.css for details.
-
Import the DialogModule
to your app.module.ts.
-
Import the ExDialog
(service) to your component ts file that calls the dialog service.
-
Inject the ExDialog
service instance, such as exDialog
, into the constructor of your component class.
constructor(private exDialog: ExDialog) { }
You can use a simple line of parameters to open a message or confirmation dialog if only default settings are needed, or only the body text, title, or icon are specified.
Syntax:
[observable-object] = exDialog.openMessage("body-text", ["title"], ["icon"]);
[observable-object] = exDialog.openConfirm("body-text", ["title"], ["icon"]);
You can also use 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:
[observable-object] = exDialog.openMessage(parameter-object);
[observable-object] = exDialog.openConfirm(parameter-object);
When calling to open any custom or data loading dialog, it’s required to pass the name of your custom dialog component together with the parameter object having needed properties.
Syntax:
[observable-object] = exDialog.openPrime(custom-component-name, parameter-object);
For the parameter object, all properties, except the message text, are optional.
Dialog Configurations
The dialog configurations can be set either on the application-level or dialog-level. The dialog-level settings take the precedence if values are set on the both levels.
Application-level Settings
If you need to apply the configuration values other than the defaults to all modal dialogs in the application, you can set the values in the caller side the same as shown the dialog-demo code.
Below are shown the partial items in the app.config.ts. The entire list can be seen in the file with some default values and necessary comments if any item name is not self-explanatory.
export const DialogConfig: any = {
topOffset: 50,
draggable: true,
animation: true,
grayBackground: true,
width: '40%',
closeDelay: 400,
closeByEnter: false,
closeByEscape: true,
closeByClickOutside: true,
- - -
};
In the app.component.ts, merge the config settings to the configuration base in the ngex-dialog/dialog-config.ts in which all default values are defined there.
import { NgExDialogConfig } from '../ngex-dialog/dialog-config';
import { DialogConfig } from './app.config';
constructor(private ngExDialogConfig: NgExDialogConfig) {
}
ngOnInit() {
this.ngExDialogConfig.appConfig = DialogConfig;
}
Dialog-level Settings
The items and values are set only for the individual modal dialog with the parameter object in the method calls. You can see many examples in the below sections.
Outlines of Dialog Core Services and Components
This article mainly focuses on the uses and issue resolutions of the ngex-dialog
. For those who are interested in the dialog internal structures and code can view these files in the ngex-dialog folder of the sample application. Listed below are role and workflow outlines of major dialog services and components for your references.
-
ex-dialog.service.ts: As the wrapper of the DialogService
, the ExDialog
class and methods directly receive the opening dialog requests and then pass the requests to the DialogService
.
-
dialog.service.ts: The DialogService
class creates the instance of the DialogHostComponent
into which the instance of the DialogMainComponent
is added. It also acts as a switchboard of closing dialogs for different situations.
-
dialog-host.Component.ts: The DialogHostComponent
class creates, caches, and removes DialogMainComponent
and passed derived instance, or multiple instances, of the DialogComponent
. When rendered, the component is related to the top-most DOM element of the dialog.
-
dialog-main.Component.ts: The DialogMainComponent
class is an invisible container for a single dialog. It handles many options and settings of the dialog, such as background overlay, animation, draggable, close dialog by clicking outside, etc. When rendered, it’s related to the DOM element between the dialog host and individual visible dialog elements.
-
dialog.component.ts: The DialogComponent
abstract
class handles all dialog parameter type definitions and value translations. It also participates in the process of closing dialogs based on conditions and callbacks. The pre-defined BasicDialogComponent
class with its template and any custom dialog component is inherited from the base DialogComponent
. Any such derived component for the dialog content is passed from the starting ExDialog
service till to DialogMainComponent
to which the real dialog content is appended. Any component derived from the DialogComponent
, when rendered, is related to the DOM elements of the real visible dialog.
Basic Use Case Examples
The ngex-dialog
uses the built-in BasicDialogComponent
with its HTML template for simple message or confirmation dialogs.
Note: When you run the sample application using the downloaded sources, the URLs may not be the same as those shown on below or other screenshots.
Opening an Information Message Dialog with Required Body Text Only
this.exDialog.openMessage("This is called from a simple line of parameters.");
Opening a Warning Message Dialog with Required Message Text Only
this.exDialog.openMessage
("This is called from a simple line of parameters.", "Warning", "warning");
Opening a Plain Dialog without Icon and Gray Background
this.exDialog.openMessage({
title: "No Icon, No Gray",
icon: "none",
message: "This is called by passing a parameter object",
grayBackground: false
});
Opening a Confirmation Dialog with Required Body Text Only
this.exDialog.openConfirm("Would you like to close the dialog and open another one?")
.subscribe((result) => {
if (result) {
this.exDialog.openMessage("This is another dialog.");
}
});
Opening a Message Dialog with Animation and Draggable Disabled
Note: the animation and draggable features are enabled for all dialogs by default.
this.exDialog.openMessage({
message: "Animation and drag-move disabled.",
animation: false,
draggable: false
});
The dialog displays 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
this.exDialog.openPrime(ProductComponent, {
width: '40%'
});
In this case, the ngex-dialog
provides the main frame features of the dialog. All contents and page-dialog communication processes are defined in the specified ProductComponent
with its template, 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.
When using the dialog box containing custom content especially dynamic loaded data list items, the dialog element height is unpredictable, which may cause the dialog box vertical alignment issues. There is conditional calculation logic in the alignment.directive.ts to handle most of the situations and show the dialog box with the ideal position (see the coding details in the file if interested). You can also fine tune the top alignment of the dialog box by specifying the topOffset
pixal value in the calling parameter list. The default topOffset
value is 50
. Experiment with your own values for the optimal vertical alignment look.
this.exDialog.openPrime(ProductComponent, {
width: '40%',
topOffset: 30
});
You can see more examples of using the custom data form and PDF Viewer IFrame with the dialogs in these links:
HTML Templates
The built-in template in the ngex-dialog
for message and confirmation types of dialogs usually meets the needs of most common uses. The HTML template code is embeded in the basic-dialog.component.ts file.
@Component({
selector: 'basic-dialog',
template: `<div class="dialog-content" #dialogElem>
<div class="dialog-header" #headerElem>
- - -
</div>
<div class="dialog-body" #bodyElem>
- - -
</div>
<div class="dialog-buttons dialog-footer" #footerElem>
- - -
</div>
</div>`
})
The themes and styles can even be customized using the settings in the ang-content/src/assets/cssex-dialog.css file from the caller application side. The Angular CLI will bundle all css files for the output based on the build configurations. Due to the code and structure compatible to npm library package edition, the HTML template and CSS files previously at component-level have been removed from the updates.
In case you need to modify the basic dialog template or add new elements into it, you can use the source-code, not the npm library package, edition of the ngex-dialog
, and then make changes for the HTML template parts in the basic-dialog.component.ts file. You can even add your own component-level basic-dialog.component.html and basic-dialog.component.css as companions of the basic-dialog.component.ts into the ngex-dialog directory.
A particular HTML template with custom dialog component class should be created for any type, other than the basic message or confirmation dialogs, such as the data form dialog. In this scenario, the dialog uses the core features of the ngex-dialog
to interact with the environment. The custom HTML template of the component is responsible for the content of the visible dialog area. Thus, any data process, communication between the HTML template and the component class, and look-and-feel of the dialog will be handled by your own code.
Closing or Keeping Open Dialogs
For a multi-level dialog tree, the Angular ngex-dialog
always creates a new object instance to open a child dialog and never re-use any parent element resource. By default, it firstly closes the parent dialog and then opens its child. It also 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:
this.exDialog.openConfirm({
. . .,
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 single OK, Go, or Continue button, the keepOpenForClose
is available when the closeButtonLabel
is used (the default setting) or the keepOpenForAction
is available when the actionButtonLabel
is used. These are more flexible than the legacy AngularJS version in which only the closeButtonLabel
and keepOpenForClose
are available for any single-button dialog.
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:
this.exDialog.openMessage({
. . .,
closeImmediateParent: true
});
If closing all dialogs:
this.exDialog.openMessage({
. . .,
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 ngex-dialog
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, refreshing the data on the parent page, or redirecting to other pages. Three options are available for running tasks when a dialog is about to be closed.
- Using custom callback function for any base screen of basic message, confirmation, or custom dialog. You can specify a callback function for the input parameter object property
beforeCloseCallback
:
let thisRef: any = this;
this.exDialog.openConfirm({
actionButtonLabel: "Continue",
closeButtonLabel: "Cancel",
message: "What next step would you like to take?",
beforeCloseCallback: function (value) {
var rtnObs = thisRef.exDialog.openConfirm({
message: "Do you really want to cancel it?"
});
return rtnObs;
}
});
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 screen that keeps everything as before when clicking the No button.
The callback function can return either the Observable
object for nested confirmation dialog box or the boolean value for any custom code logic. If you use a callback function for closing a custom dialog, returning true
will close the dialog and false
keep the dialog open. You can view the example of using a dialog callback function to refresh parent data sheet in my Angular Data CRUD article.
- Using the returned
Observable
result directly from a confirmation dialog. Usually, we set the result to false
for the cancel operation. Additional tasks can be performed in the “else
” code block.
this.exDialog.openConfirm("Would you like to close the dialog and open another one?")
.subscribe((result) => {
if (result) {
this.exDialog.openMessage("This is another dialog.");
}
else {
this.exDialog.openMessage("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 previous parent dialog screen.
-
When using any data dialog with a custom template, you can also place any code logic directly in the close button function call. This approach is pretty straightforward since the close button and its attributes are specified within the custom template. Here is the cancel()
function code for the cancel warning and confirmation in the data form dialog example.
cancel() {
this.exDialog.openConfirm({
title: "Cancel Warning",
icon: "warning",
message: "Do you really want to cancel the data editing?",
keepOpenForAction: true,
keepOpenForClose: true
}).subscribe((result) => {
if (result) {
thisRef.exDialog.openMessage({
title: "Notification",
message: "The editing has been cancelled.",
closeAllDialogs: true
});
}
else {
thisRef.exDialog.openMessage({
title: "Notification",
message: "The editing will continue.",
closeImmediateParent: true
});
}
});
}
The screenshot shows the result when clicking No button on the Cancel Warning dialog:
Unlike using the callback function, this logic cannot automatically trigger any call on the parent page. You need to use one of the approaches between Angular component interactions to conduct dialog caller's actions in response to the dialog closing.
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 ngex-dialog
is fully draggable and well adaptable to the screen resizing. The draggable option is enabled by default. You can turn off this feature for the entire application by changing the default value to false
in these files:
- dialog-config.ts file if you use the source-code edition
- app.config.ts file of the caller application if you use either source-code or npm library package edition
For any individual dialog, you can disable it by passing the “draggable: false
” in the parameter object as the example shown before.
Some behaviors associated with draggable dialogs are still worth mentioning here.
-
When enabling the draggable for any dialog with a custom component containing input type elements, you need to specify additional (focus)
and (blur)
event attributes for each input element as in the product.component.html example:
<input type="text" class="form-control" id="txtProductName" name="txtProductName"
[(ngModel)]="model.product.ProductName" required maxlength="50"
(focus)="setDrag(true)" (blur)="setDrag(false)" />
This is because the draggable directive in the ngex-dialog
doesn’t call the preventDefault()
function of the mousedown
event which, if called, disables the input fields on the dialog. But with HTML default settings, 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 global flag is set in the dialog-cache.ts 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 so that text highlighting with the mouse can be performed:
-
There is no more the issue of selected display text on the dragged dialog and underlying page that was seen in the legacy AngularJS version, even the dialog is dragged and moved out of the window’s edges. This is due primarily to the use of custom focus-blur
directive that is placed for any input element on the open dialog. When the dialog opens, the focus is switched immediately to the element of the dialog from the underlying page or parent dialog. It is then automatically out of, or keeping, focus based on the passed value. In the example, this code line is used for the first button on the dialog.
<button type="button" class="close" [focus-blur]="'focus_blur'"
(click)="cancel()">×</button>
The logic in the focus-blur.directive.ts is:
if (this.option == "focus" || this.option == "focus_blur") {
this.element.nativeElement.focus();
}
if (this.option == "blur" || this.option == "focus_blur") {
this.element.nativeElement.blur();
}
-
Resizing the window will always re-center the dialog within the window if the dialog has not been dragged since it opens. This behavior is not changed comparing to the legacy AngularJS version. For the Angular ngex-dialog
, if the dialog is dragged and then the window is resized, the dialog is also moved proportionally to the previous relative position. This improvement is due mainly to the new code structures and calculations for the window resize in the AlignmentDirective
class. You can read the code in the alignment.directive.ts for details.
When reducing the size of the window after dragging an open dialog to near the right bottom corner, the dialog will still be near the right bottom corner area and not be partly or entirely out of the edges.
Custom Styles for Built-in Template
Additional CSS classes can be specified for elements of the basic message or confirmation dialog with the build-in template. For example, if a particular dialog needs some header and footer styles other than the defaults, we can add the properties, headerAddClass
and footerAddClass
, to the parameter object and then create corresponding CSS classes:
exDialog.openMessage({
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 element CSS classes is coded in the dialog.component.ts and listed as comments in the dialog-config.ts (or caller-side app-config.ts). In the dialog input parameter object, some properties for setting basic dialog CSS styles have no default values. If you directly use the source-code edition of the tool and it’s required to make all dialogs the same look-and-feel across the entire application, you may change the CSS classes to your own ones in the HTML templates. For either source-code or npm library package edition, you can also add your own styles or edit existing styles in the ex-dialog.css of your caller application (see example of the ex-dialog.css under the ang-content/src/assets/css/ folder if you need the different styles for the base dialog components and associated context.
Closing Dialogs with Browser Navigations
On Angular-coded pages, any browser redirection to another site will automatically close any open dialog. However, browsing back and forward on a page that has any history activity and any open dialog could keep the current modal dialog still shown over the re-visited page background. In the legacy AngularJS version, I used the $locationChangeStart
event handler to close any open dialog before switching back or forth to other pages. To resolve the same issue in Angular, we can firstly use the router’s NavigatoinEnd
event handler to obtain the old and new route locations. We can then call to remove any existing cached dialog components in the dialogs
array when the route location changes. The best place to add this code logic is the ngOnInit()
method of the AppComponent
class in the app.component.ts file:
ngOnInit() {
let pThis: any = this;
this.router.events.pipe(
filter(value => value instanceof NavigationEnd),
pairwise()
).subscribe((value: any) => {
if (value[1].url != value[0].url) {
let dialogs: any = pThis.exDialog.getDialogArray();
if (dialogs != undefined && dialogs.length > 0) {
pThis.exDialog.clearAllDialogs(dialogs[dialogs.length - 1])
}
}
});
}
The below screenshots illustrate the browser-back operations on the resolved issue.
When browsing to the Second Sample page, open a dialog on that page and then click the browser back button:
The dialog is automatically closed and the process returns to the first main page:
Summary
It’s my pleasure to share with the developer's communities the code of the Angular modal dialog, ngex-dialog
, the demo sample project, and discussions in the article. The modal dialog tool as the client-side code works on any website project although the full sample project uses the Microsoft ASP.NET MVC 5 or ASP.NET Core structures. Audiences can see the other more advanced samples using the ngex-dialog
from these posts:
-
28th March, 2017
-
9th July, 2017
-
2nd September, 2017
- Added sample code for the Angular version 4
- Changed names "Angular 2" and "Ng2ExDialog" to "Angular" and "NgExDialog", respectively, in the article title and text
-
14th January, 2018
-
22nd November, 2018
- Update source code with the Angular 6 CLI
- Edited text in some sections and changed all screenshots of the article
- If needed, the source code and setup instructions for Angular 5 can be downloaded here
-
31st March, 2019
- Updated source code with the Angular 7 CLI and Bootstrap 4.3
- The text in the first two sections is also edited accordingly
- The previous source code in Angular 6 and Bootstrap 3.3 can be downloaded here
-
5th October, 2019
- Updated source code with Angular 8 CLI
- Added instructions to check the TypeScript version for the Visual Studio
- Added adjusting vertical alignment with
topOffset
value - The previous source code in Angular 7 and Bootstrap 4.3 can be downloaded here
-
11th December, 2020
- Updated source code with the Angular 11 CLI
- Updated website project with the ASP.NET Core 5.0
- Edited article text in some sections for the updates
- If you need to run the sample application with previous Angular version 8, 9, or 10, you can download the package.json file for the application, Package.json_Ng8-9-10.zip, replace the package.json file in the existing application with the version you would like, than do the same based on the instructions in the Setting up Sample Application section. The Angular 11 source code of the sample application is fully compatible with the Angular version 8, 9, and 10 without major breaking changes.
-
15th February, 2021
- Major source code updates on the
ngex-dialog
to compatible with the npm library package edition. - Changes in the article sections and text corresponding to source code updates.