Tributes
Special thanks to Roy Drenker, who inspired me to the implementation demonstrated in this article.
The problem
When it comes to modal dialogs in your Web application, you of course want something as easy to use as the native JavaScript confirm function:
if (confirm(‘Do you agree?’)) { do_it(); }
The problem with the native alert/confirm/prompt functions is that they are very limited in their functionality and customization options.
Being an Angular developer, you might dream about the possibility of creating dialogs whose content and behavior can be defined by convenient Angular means. Perhaps showing the dialog could be wrapped in a single function call, and asynchronous processing of dialog results could be implemented with simple and clear code.
In this article, I’ll share with you a relatively simple implementation of a dialog function library that tries to match these requirements.
Dialog content and behavior in this implementation can be represented by an arbitrary Angular component. A dialog can be shown by a dedicated function that returns a dialog result as a Promise object.
Promise
This article doesn’t try to explain Promises. It supposes that you have a basic knowledge of this technology. If it’s not the case, there are plenty of good articles around that explain Promise in fine detail, for example this one:
https://developers.google.com/web/fundamentals/getting-started/primers/promises
But because the article requires very basic knowledge of Promises, I’ll remind you of these basics:
Promise is an object that allows one piece of code that waits for an asynchronous execution result (I call it Promise client) to communicate with the other piece of code that produces this result (I call it Promise provider). The Promise provider creates the Promise object, and Promise client can “subscribe” to this result using code like this:
promise.then(resultCallback).catch(rejectCallback)
The line above is non-blocking; it returns control immediately and execution continues on the next line of code.
When a Promise provider realizes that it has a result (for example an XMLHttpRequest is complete), it performs special actions that causes Promise to call Promise client’s resultCallback with result data. In a case where the Promise provider detects that something went wrong (for example an XMLHttpRequest has failed), it calls the rejectCallback with error data.
The result data passed to the resultCallback can in turn be a Promise, which means that you can create chains of the .then().catch() calls:
promise.then(result1Callback).then(result2Callback).catch(reject1Callback)
.then(result3Callback).catch(reject2Callback)
Basic architecture
The core showComponentInPopup function
The core of the implementation is a low level showComponentInPopup function which is capable of:
- Creating an instance of an arbitrary Angular component, initializing its properties with specific values, and showing it in a dialog window (probably modal) on a screen.
- Returning a result of a user interaction with a dialog as a Promise.
So you could write something like this:
showComponentInPopup(dialogWindowProperties, componentToShow, componentProperties)
.then(result => {/*process dialog result*/})
.catch(error => {/*process error*/});
Specific dialog functions
Having this showComponentInPopup function in place, we can create a library of the specific dialog functions like:
yesNoDialog('Do you really want it?')
.then(() => /*do something if Yes*/)
.catch(() => /*do something if Not*/);
or
loginDialog(ownerElement)
.then((data) => /*process successful authorization*/)
.catch((error) => /*process authorization failure*/);
…and a lot of others.
For this, we should:
- Create a specific Angular component that will represent the dialog content, contain the result of a user’s dialog interaction, and maybe the result of the dialog’s interaction with some services.
- Create a shortcut function that will call the showComponentInPopup function and pass the component type and its initialization property values as the function parameters.
Implementation
We’ll use the following plunker as a working example of the implementation discussed in this article:
http://plnkr.co/edit/znCi0O6Wh3x3Te7b1rqr?p=preview
showComponentInPopup function
The function, as well as the library of the shortcut functions, is implemented in the app/services/Dialogs.ts file of the sample. You can find its full code following:
import { Component, EventEmitter, Inject, ElementRef, Injector, forwardRef, ViewChild,
Input, ComponentFactoryResolver, ViewContainerRef, NgModule} from '@angular/core';
import * as ng2Core from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import * as wjcInput from 'wijmo/wijmo.input';
import { WjInputModule } from 'wijmo/wijmo.angular2.input';
/*
* Shows an Angular component in Wijmo Popup.
*
* @param popupOptions A hash object defining Popup property values, the same as the 'options' parameter of Popup constructor.
* @param componentType The type of the component to show in popup.
* @param componentProperties A hash object containing the component property value.
* @param vcr An ViewContainerRef object of the component calling this function, can be acquired via component's
* constructor parameter.
* @param cmpResolver A ComponentFactoryResolver object of the component calling this function, can be acquired via component's
* constructor parameter.
*/
export function showComponentInPopup(popupOptions: any, componentType: any, componentProperties: any,
vcr: ViewContainerRef, cmpResolver: ComponentFactoryResolver): Promise<any> {
// create and initialize Wijmo Popup
let hostEl = document.createElement('div'),
popup = new wjcInput.Popup(hostEl);
popup.hideTrigger = wjcInput.PopupTrigger.None;
popup.initialize(popupOptions);
// create the dialog content component instance,
// and initialize its properties
let cmpRef = vcr.createComponent(
cmpResolver.resolveComponentFactory(componentType));
if (componentProperties) {
for (let prop in componentProperties) {
cmpRef.instance[prop] = componentProperties[prop];
}
}
// add the component to the Popup
popup.content = cmpRef.location.nativeElement;
// add handler to the popup.hidden event that will
// destroy the popup and contained component
let hiddenEh = () => {
popup.hidden.removeHandler(hiddenEh);
cmpRef.destroy();
popup.owner = null;
hostEl.parentElement.removeChild(hostEl);
popup.dispose();
}
popup.hidden.addHandler(hiddenEh);
// show the popup
popup.show(false);
// the function's return value, assigned with a Promise
// representing dialog result
let ret = (<IPromiseResult>cmpRef.instance).result;
// Add this .then.catch branch to the returning promise
// that hides the popup after the component's 'result'
// will be resolved or rejected.
// Note that this branch is not visible to the client code.
ret.then((data) => {
popup.hide();
return data;
})
.catch((error) => {
popup.hide();
})
return ret;
}
Let’s walk through this implementation and discuss some of its key parts.
Popup
We need some UI means which are capable of showing content in a modal “window”. In our example, we use a Wijmo Popup control for this purpose.
http://wijmo.com/5/docs/topic/wijmo.input.Popup.Class.html
You are free to use any other tools like Bootstrap modal popups. The first parameter of the showComponentInPopup function is popupOptions, where you can pass a hash object with Popup specific property values. For example, you may want to show a dialog anchored to some DOM element, instead of centering it on the screen. In this case, you may pass
{ owner: anchorElement }
as the parameter value, which will instruct the Popup to visually stick to the
anchorElement.
The following piece of code creates and initializes a Popup instance with the passed Popup property values:
let hostEl = document.createElement('div'),
popup = new wjcInput.Popup(hostEl);
popup.hideTrigger = wjcInput.PopupTrigger.None;
popup.initialize(popupOptions);
Component as a dialog content
Now we should instantiate a component which is used as dialog content and initialize its properties. The function defines two obvious parameters related to the content component – componentType, which receives the type (a reference to a constructor function) of a component, and componentProperties, where you can pass a hash object with the component property values.
But there are also two not so obvious parameters in the function definition:
vcr: ViewContainerRef,
cmpResolver: ComponentFactoryResolver
These parameters are references to the Angular services necessary to dynamically (in code) create an instance of a component. They can be obtained by injecting them as parameters of a constructor of a component that will call dialog functions. Creating components dynamically is described in the Angular documentation here:
https://angular.io/docs/ts/latest/cookbook/dynamic-component-loader.html
In fact, this translates into the following line in our code:
let cmpRef = vcr.createComponent(
cmpResolver.resolveComponentFactory(componentType));
This code creates a component and assigns the associated
ComponentRef instance (holding some useful information about the component) to the
cmpRef variable. The
cmpRef.instance property contains a reference to the instantiated component, and
cmpRef.location.nativeElement can be used to retrieve the root DOM element representing the instantiated component’s template.
After the component has been created, we assign its properties with the values passed in the
componentProperties parameter:
if (componentProperties) {
for (let prop in componentProperties) {
cmpRef.instance[prop] = componentProperties[prop];
}
}
After that we add the component’s DOM to the Popup, so that the popup’s content will be the component’s template:
popup.content = cmpRef.location.nativeElement;
After that we can show the component on the screen:
popup.show(false);
Promise as a dialog result
Any JavaScript custom dialog implementation is asynchronous by its nature. You can’t implement it in the same way as the native alert, confirm, and prompt functions, where code execution is blocked until a user presses a button in the dialog. Our dialogs have to return their results at unpredictable moments dictated by a user, or even by some asynchronous services in more complex scenarios. For example, when a user presses a confirmation button in a LoginDialog, the latter could send an authentication request to the server, and wait for a response. Only after the response is received will the dialog result be ready for further processing.
The modern and probably most convenient way to handle asynchronous results is provided by Promises. If an asynchronous function result is a Promise object, the client code should just add a .then(…).catch(…) chain of calls to the function, where the handler functions processing the results are passed as parameters. This is the approach we’ll use in our implementation.
The dialog results are created by our custom dialog content components and are specific to the components’ semantics. The result can be exposed as a component property of the Promise type. Our showComponentInPopup function should know what the property is, to be able to read this result from an arbitrary component and return it as a function result. That is, we need to sign a contract between the function and the content component that will allow the function to know how to retrieve the result from the component.
We make this contract formal by introducing the following interface:
export interface IPromiseResult {
readonly result: Promise<any>;
}
Any dialog content component should implement this interface, which will store the dialog result as a Promise object returning by the
result property.
After putting this agreement into play, we can easily retrieve the dialog result from the component and return it as the function result:
let ret = (<IPromiseResult>cmpRef.instance).result;
return ret;
But our
showComponentInPopup function must implement one more obvious piece of functionality – it has to close the popup after the dialog result is ready (that is the Promise will be resolved or rejected). It would be wrong to put this responsibility to a code that calls the function.
For this, the function adds its own .then().catch() chain to the returning Promise, where it hides the popup. The complete code snippet that deals with this looks as follows:
let ret = (<IPromiseResult>cmpRef.instance).result;
// Add this .then.catch branch to the returning promise
// that hides the popup after the component's 'result'
// will be resolved or rejected.
// Note that this branch is not visible to the client code.
ret.then((data) => {
popup.hide();
})
.catch((error) => {
popup.hide();
})
return ret;
So, the function has added a
.then().catch() chain to the returning promise, and an application code that calls the function will definitely add a similar
then().catch() chain to the same Promise, in order to process the dialog results. Two different chains on the same Promise? Yes, and it’s absolutely legal! With this, we come to a consideration of the technique called Promise branching. Let’s investigate it.
The diagram below graphically depicts the situation:
![Promise Architecture]()
When result is ready (Promise resolved or rejected), then both .then().catch() chains will be executed (the topmost probably the first), and perform their job without interfering with each other – showComponentInPopup will hide the popup, and the chain in the application code will perform the required processing.
Dialog functions library
Now, having the showComponentInPopup function in place, let’s see how we can implement a dialog function library using it. The library is represented by the Dialogs class with static methods. You may find the implementation in the app/services/Dialogs.ts file.
Confirmation dialogs
This is an analogue of the native confirm function, representing a dialog with a confirmation text and two buttons (e.g. Ok/Cancel) that allows a user to confirm or reject an action. We’ll implement two different functions, okCancelDialog with Ok/Cancel buttons, and yesNoDialog with Yes/No buttons. The confirmation text is passed as the functions’ parameter.
The first step in the process of creating a dialog function is to implement a component that represents the dialog content. For this we create the OkCancelCmp component, whose code looks like:
@Component({
selector: 'ok-cancel-cmp',
template: `
<div style="padding:10px 10px">
{{question}}
<div>
<button (click)="clicked(true)">{{okCaption}}</button>
<button (click)="clicked(false)">{{cancelCaption}}</button>
</div>
</div>
`
})
export class OkCancelCmp implements IPromiseResult {
question: string;
okCaption = 'Ok';
cancelCaption = 'Cancel';
private _result: Promise<any>;
private _resolve: any;
private _reject: any;
constructor() {
this._result = new Promise((resolve, reject) => {
this._resolve = resolve;
this._reject = reject;
});
}
get result(): Promise<any> {
return this._result;
}
clicked(ok: boolean) {
if (ok) {
this._resolve();
} else {
this._reject();
}
}
}
The confirmation text and button captions are represented by the question,
okCaption, and
cancelCaption properties (the latter two are assigned with ‘Ok’ and ‘Cancel’ by default). They can be changed by assigning them with other values.
The dialog result, according to our contract represented by the IPromiseResult interface, is exposed as the result property of the Promise type. Because this dialog result supposes only two states, confirmed or canceled, depending on which button user has pressed, it’s convenient to consider the confirmation as a resolved Promise, and cancellation as a rejected Promise. So, an application code that uses the dialog might looks like this:
okCancelDialog('Do this?').then(okDelegate).catch(cancelDelegate);
The okDelegate function will be called if a user presses the Ok button, and cancelDelegate will be called if Cancel was pressed.
We create the Promise stored in the result property in the component constructor:
constructor() {
this._result = new Promise((resolve, reject) => {
this._resolve = resolve;
this._reject = reject;
});
}
The Promise constructor accepts a function with two parameters, resolve and reject, that will receive references to callback functions that client code has specified in
.then() and
.catch() calls. That is, provided the example above,
okDelegate will be passed as the resolve parameter value, and
cancelDelegate will be passed in the reject parameter.
Our responsibility as the Promise object provider is to:
- Call resolve when Promise result is ready (i.e. user has pressed the Ok button). This will execute the delegate passed by the client code in the .then() call.
- Call reject when Promise is rejected (i.e. user has pressed the Cancel button). This will execute the delegate passed by the client code in the .catch() call.
For this, we store resolve and reject parameter values in the component private properties (_resolve and _reject respectively). Then, we add the clicked event handler method which is bound to the buttons in the template, like here:
<button (click)="clicked(true)">{{okCaption}}</button>
<button (click)="clicked(false)">{{cancelCaption}}</button>
In this method we just call _resolve or _reject depending on the passed parameter value (ok or not ok).
clicked(ok: boolean) {
if (ok) {
this._resolve();
} else {
this._reject();
}
}
That’s it, we’re done with the dialog content component implementation.
Now we can easily add the dialog functions.
okCancelDialog:
static okCancelDialog(question: string, vcr: ViewContainerRef,
cmpResolver: ComponentFactoryResolver): Promise<any> {
return showComponentInPopup({}, OkCancelCmp,
{ question: question },
vcr, cmpResolver);
}
It calls
showComponentInPopup and passes the content component type (
OkCancelCmp) as a parameter. It also passes the component property values, only the question property value in this case, whose value it receives in the same named function parameter:
{ question: question }
And it passes
ViewContainerRef and
ComponentFactoryResolver objects that we discussed above, which it receives in function parameters.
yesNoDialog:
static yesNoDialog(question: string, vcr: ViewContainerRef,
cmpResolver: ComponentFactoryResolver): Promise<any> {
return showComponentInPopup({}, OkCancelCmp,
{ question: question, okCaption: 'Yes', cancelCaption: 'No' },
vcr, cmpResolver);
}
This is a variation of the
okCancelDialog function with different button captions – Yes and No. The only difference here is that in addition to the question property value, we also pass button caption values to the content component:
{ question: question, okCaption: 'Yes', cancelCaption: 'No' },
Making the library an NgModule
There is one specific nuance with dynamically created Angular components that we should address. It lies in that any component created by the ViewContainerRef.createComponent method should be added to some NgModule’s entryComponents metadata property. To satisfy this requirement, we create a special DialogsModule module in the app/services/Dialogs.ts file, which references all the components used as dialog content in our dialog function library. The definition looks like the following code:
@NgModule({
imports: [BrowserModule, WjInputModule],
declarations: [LoginCmp, OkCancelCmp],
entryComponents: [LoginCmp, OkCancelCmp],
})
export class DialogsModule {
}
Having this NgModule, any application module that uses the library just needs to import
DialogsModule in its NgModule definition.
Using the confirmation dialog functions in an application.
The function’s usage is demonstrated in the app/app.ts file, AppCmp component.
First, we need to import DialogsModule in the NgModule that AppCmp belongs to:
import { DialogsModule, Dialogs } from './services/Dialogs';
...
@NgModule({
imports: [WjInputModule, WjGridModule, BrowserModule, FormsModule,
DialogsModule],
declarations: [AppCmp],
providers: [DataSvc],
bootstrap: [AppCmp]
})
export class AppModule {
}
Then, we need to obtain references to the ViewContainerRef and ComponentFactoryResolver Angular services, as was discussed above. We do it by injecting them in the component’s constructor parameters:
constructor( @Inject(ViewContainerRef) private _vcr: ViewContainerRef,
@Inject(ComponentFactoryResolver) private _cmpResolver: ComponentFactoryResolver) {
}
Now we can use them:
yesNoDialog() {
Dialogs.yesNoDialog('Do you really want it?', this._vcr, this._cmpResolver)
.then(() => alert(`You answered YES`))
.catch(() => alert(`You answered NO`));
}
This call will alert ‘You answered YES’ if users clicks the Yes button, and ‘You answered NO’ if user clicks the No button.
okCancelDialog() {
Dialogs.okCancelDialog('Do you confirm this action?',
this._vcr, this._cmpResolver)
.then(() => alert(`You confirmed action`))
.catch(() => alert(`You cancelled action`));
}
This call will alert ‘You confirmed action’ if users clicks the Ok button, and ‘You cancelled action’ if user clicks the Cancel button.
Using the confirmation dialog functions in an application
The functions usage is demonstrated in the app/app.ts file, AppCmp component.
First, we need to import the DialogsModule in the NgModule that AppCmp belongs to:
import { DialogsModule, Dialogs } from './services/Dialogs';
…
@NgModule({
imports: [WjInputModule, WjGridModule, BrowserModule, FormsModule,
DialogsModule],
declarations: [AppCmp],
providers: [DataSvc],
bootstrap: [AppCmp]
})
export class AppModule {
}
Then, we need to obtain references to the
ViewContainerRef and
ComponentFactoryResolver Angular services, as was discussed above. We do it by injecting them in the component’s constructor parameters:
Login dialog simulator
This is not a fully functional login dialog implementation, for simplicity’s sake we offer just a simulator of such a dialog with the following core functionality – when a user presses a confirmation button in the dialog, the latter sends a request to a server, and depending on the authentication results (success or failure) resolves or rejects the Promise representing the dialog result.
Our login simulator dialog contains two buttons for this – Simulate success and Simulate failure.
The dialog content is represented by the LoginCmp component that has the following implementation:
export class LoginCmp implements IPromiseResult {
private _result: Promise<any>;
onResult = new EventEmitter();
constructor() {
this._result = new Promise((resolve, reject) => {
this.onResult.subscribe((success) => {
if (success) {
resolve('Done');
} else {
reject('Access denied');
}
});
});
}
get result(): Promise<any> {
return this._result;
}
simulate(success: boolean) {
setTimeout(() => {
this.onResult.next(success);
}, 0);
}
}
We use a different technique here to resolve or reject the resulting promise. The component declares the
onResult event:
onResult = new EventEmitter();
This event will be triggered when authentication results have been received from a server. The event value keeps the authentication results (a success/failure boolean value in our case). The event is triggered in the simulate method, with a timeout that simulates an asynchronous communication with the server.
The result Promise is created in the constructor. The Promise callback function subscribes to the onResult event and resolves or rejects it depending on the event data value. The Promise is resolved with a data (the ‘Done’ string), or rejected with an error data (‘Access denied’), that can be inspected by the code that calls the dialog.
The content component is done, the last step is to create a dialog function, loginDialog:
static loginDialog(anchor: HTMLElement, vcr: ViewContainerRef,
cmpResolver: ComponentFactoryResolver): Promise<any> {
return showComponentInPopup({ owner: anchor }, LoginCmp, {},
vcr, cmpResolver);
}
In contrast to the
okCancelDialog function that displays the dialog centered on the screen, for
loginDialog we want to be able to show it visually anchored to some element, e.g. to a button that triggered the dialog. The element can be specified in the anchor parameter, and we assign it to the owner property of the Popup by passing Popup options object as the first parameter of the
showComponentInPopup function:
{ owner: anchor }
Now we can use the function in our application.
HTML:
<button #button1 (click)="loginDialog(button1)">
Login
</button>
Click event handler:
loginDialog(owner: HTMLElement) {
Dialogs.loginDialog(owner, this._vcr, this._cmpResolver)
.then((data) => alert(`Logged in! Status: ${data}`))
.catch((error) => alert(`Login failed. Error: ${error}`));
}
Note that we specify the button1 element as the owner of the dialog, so the dialog will appear visually anchored to the button, instead of being centered on the screen.
The .then() handler function uses the data parameter that holds a resolved promise data, and the .catch() handler function uses the error parameter containing an error information for the rejected Promise.
Conclusion
In this article, we discovered one possible way to implement modal dialogs shown by function calls, whose content and behavior are defined by arbitrary Angular components, and whose dialog results are represented by Promise objects. You can use this solution for your personal needs.
As a tool for showing dialogs we used a Wijmo Popup control:
http://wijmo.com/5/docs/topic/wijmo.input.Popup.Class.html
The article was accompanied by the following sample:
http://plnkr.co/edit/znCi0O6Wh3x3Te7b1rqr?p=preview
The post Implementing modal dialog functions with Promise based dialog results in Angular appeared first on Wijmo.