An Angular 2 Modal Dialog with Advanced Functionality and Easy-use Features

0
84

Introduction

Upgrading a web application from Angular 1.x to Angular 2 involves re-structuring and code re-writing processes. But it’s an interesting work on re-writing my previous posted Angular 1.x model dialog NgExDialog to the Angular 2 version and also creating the sample application project. This article is not only for sharing the new modal dialog code but also providing discussions on the uses and issue resolutions of the modal dialog.

The Ng2ExDialog has these features that are the same or better than its ancestor NgExDialog:

  • 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.
  • 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 single folder containing all TypeScript, CSS, and HTML files for the dialog service.

Below are all files listed for the Ng2ExDialog:

The major dependencies of an Angular 2 web application all apply to the Ng2ExDialog. Please check the package.json file in the sample project to view those dependent library tools and files.

Setting up Sample Application

You can download the Ng2Exdialog sample source code files listed above based on what project types and Visual Studio versions you would like to use.  Due to the pure client UI coding structure, the Ng2ExDialog works the same if you place the folder and files to any website project that supports HTML, CSS, Javascript, and TypeScript.

For using the ASP.NET Core project type, you need to follow these steps to make the sample application work on your local machine.

  1. Check the installations of the node.js and npm. I created and tested the sample application with the node.js version 6.2.2 to 6.9.5 and npm version 3.9.5 to 3.10.10. Any newer versions may work but I haven’t tried.

  2. Make sure that, in the Visual Studio > Tools > Options > Projects and Solutions > (for Visual Sudio 2017 only: Web Package Management > ) External Web Tools section, the “.\node_module\.bin” is the topmost and checked item.


  3. If running the sample application with Visual Studio 2015, you need to confirm the installation of two library tools on your local machine listed below. These tools with updated and correct versions are already available for Visual Studio 2017.


    • ASP.NET Core 1.0 SDK. In the downloaded source for the Visual Studio 2015, the version “1.0.0-preview2-1-003177” is specified in both the global.json and project.json files. A sub-folder of the version name must exist under the C:\Program Files\dotnet\sdk\ directory although you can change to other SDK versions.

    • TypeScript for Visual Studio 2015 version 2.0.3 to 2.2.2. You may get design-time and build errors without the installation and links to the C:\Program Files (x86)\Microsoft SDKs\TypeScript\2.0 or C:\Program Files (x86)\Microsoft SDKs\TypeScript\2.1 folder. The installed TypeScript version in the original Visual Studio 2015 is 1.8.34 which may not work well here.

  4. When opening the sample application first time with the Visual Studio, The npm will automatically download files of dependencies based on the list in the package.json. After the file downloading completes and if the Task Runner Explorer is not open there, press Ctrl + Alt + Backspace keys (or View menu > Other Windows > Task Runner Explorer) to open the Task Runner Explorer, right-click copyLibs, and then select Run. The copyLibs task needs to run manually anytime when any reference in the package.json is changed (We would not like to have these relative static references updated during each project rebuild). 


  5. Rebuild the solution by press F5. All deployed client-side files will also automatically be copied to the site root, the wwwroot folder.

  6. Right-click the wwwroot  > View Browser in the Solution Explorer or click the IIS Express from the toolbar to start the sample application. If you pre-select the Internet Explorer (VS 2015 and VS 2017) or Google Chrome (VS 2017 only) under the IIS Express, click the IIS Express can start the debugging run session directly on the TypeScript/JavaScript code lines with any pre-set breakpoint. You don’t have to do the client code debugging with the browser F12.

The sample code for an ASP.NET 5 project works the same when using either Visual Studio 2017 or 2015 (Update 3) as long as the version of installed TypeScript for Visual Studio is from 2.0.3 to 2.2.2 which is simply for the design-time code verification and project build. To set up the sample application with the ASP.NET 5 project, follow these instructions.

  1. You still need the node.js and npm installed on your machine as mentioned above.

  2. Open the solution with the Visual Studio 2017 or 2015 and then open the Solution Explorer.

  3. Right-click the package.json under the SM.Ng2ExDialog.Sample project and then select the Restore Packages. The npm will automatically download all required client script libraries.

  4. Press Ctrl + Alt + Backspace keys to open the Task Runner Explorer, right-click copyLibs, and then select Run. The sample code for the ASP.NET 5 project uses the gulpfile.js and Task Runner only for copying the client script library files from the node_modules to the Scripts/libs. All application content files are already in the running locations. Note that whenever you have updated any required client script library, you need to re-run the copyLibs task.

  5. Press F5 to run the sample application.

Dialog Access Scenarios and Syntax

The Ng2ExDialog is built as an Angular 2 service provider. You can see all demo cases in sections later. For using the Ng2ExDialog in your own project, you simply need to do the following (also see the details in the source code files):

  • Copy the entire Ng2ExDialog folder to your development component locations.
  • Add the below css file links to your index.html file (you can check files under the wwwroot folder of the sample application for references).


    <link rel="stylesheet" href="[location of your bootstrap.css version 3.3.7 file]">
    <link rel="stylesheet" href="app/Ng2ExDialog/Css/ex-dialog.css">

  • 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.

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:

[[__em__]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:

[[__em__]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:

[[__em__]observable-object] = exDialog.openPrime(custom-component-name, parameter-object);

For the parameter object, all properties, except the message text, are optional. The default values of the properties are set in the dialog.config.ts file. You can also view all available property names in this file and the dialog.component.ts file. A description is always there for Each property item if it isn’t self-explanatory.

Outlines of Dialog Core Services and Components

This article mainly focuses on the uses and issue resolutions of the Ng2ExDialog. For those who are interested in the dialog internal structures and code can view these files in the Ng2ExDialog 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 are all 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 Ng2ExDialog uses the built-in BasicDialogComponent with its HTML template for simple message or confirmation dialogs.

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 the 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 the animation and draggable disabled (Note that 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: '450px' 
});

In this case, the Ng2ExDialog 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.

HTML Templates

The built-in template in the Ng2ExDialog 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 individual element level. In case you need to modify it or add new elements into it, you can make changes in the basic-dialog.component.htmlbasic-dialog.component.css, and basic-dialog.component.ts files.

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 Ng2ExDialog 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.

The relative URL of any HTML template specified for the templateUrl of the component can automatically be resolved due to setting the id property of the NodeModule in the @Component decorative. This is really a nice feature of the Angular 2. For example, the basic-dialog.component.html is physically deployed to the “/app/Ng2ExDialog” folder whereas in the TypeScript file, we just need to set the templateUrl to the current relative path.

@Component({
 moduleId: module.id,
 selector: 'basic-dialog',
 templateUrl: "./basic-dialog.component.html",
 styleUrls: ["./basic-dialog.component.css"] 
})

Closing or Keeping Open Dialogs

Like its ancestor NgExDialog in a multi-level dialog tree, the Ng2ExDialog 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. For the Angular 1.x NgExDialog, 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 Ng2ExDialog 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 about to be closed.

  1. Using custom callback function for any base screen of basic message or confirmation 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.


  2. Using the returned Observable result 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.

  3. Opening a confirmation dialog directly from the close button function call for any data 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 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:


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 Ng2ExDialog is fully draggable and well adaptable to the screen resizing. The draggable option is set by default. You can turn off this feature for the entire application by changing the default value to false in the dialog-config.ts file or disable it for any individual dialog 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 although issues for the Ng2ExDialog seem less than those of Angular 1.x version.

  1. 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 Ng2ExDialog 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 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:


  2. There is no more the issue of selected display text on the dragged dialog and underlying page that was seen in the Angular 1.x 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()">&times;</button>


    The logic in the focus-blur.directive.ts is:


    if (this.option == "focus" || this.option == "focus_blur") {
 this.renderer.invokeElementMethod(this.element.nativeElement, 'focus');
    }
    if (this.option == "blur" || this.option == "focus_blur") {
 this.renderer.invokeElementMethod(this.element.nativeElement, 'blur');
    }

  3. 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 Angular 1.x version. For the new Ng2ExDialog, 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 VerticalCenterDirective class. You can read the code in the vertical-center.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 creating 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 listed in the dialog-config.ts and dialog.component.ts files. In the dialog input parameter object, some properties for setting basic dialog CSS styles have no default values. If 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 directly in the HTML templates.

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 Angular 1.x 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 2, we can firstly use the router’s NavigatoinEnd even 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 constructor of the AppComponent class in the app.component.ts file:

constructor(private router: Router, private exDialog: ExDialog) {
 
 this.router.events.filter(value => value instanceof NavigationEnd)
 .pairwise().subscribe((value) => {
 if (value[1].url != value[0].url) {
 
 let dialogs = this.exDialog.getDialogArray();
 if (dialogs != undefined && dialogs.length > 0) {
 
 this.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 2 modal dialog, Ng2ExDialog, the demo sample project, and discussions in the article. The modal dialog tool as the client-side code works on any website project if you take all files in the Ng2ExDialog folder from the sample project although the full sample project uses the Microsoft ASP.NET Core 1.0 or ASP.NET 5 structures. Either the Ng2ExDialog or demo application is built with the high quality and real-world scenarios in mind. Because an Angular 2 web application poses high dependencies on other libraries and tools, audiences may encounter some problems of running the sample application at very first time. But the code should work fine if following the instructions in the Setting up Sample Application section. Hope that developers would like the Ng2ExDialog. Any feedback will be welcome.

History

  • 3/28/2017: Original post.
  • 7/9/2017: Added sample code and setup instructions for the ASP.NET 5 project type.

LEAVE A REPLY