Skip to main content Skip to footer

Implementing Modal Dialog Functions with Promise-based Dialog Results in Angular

Quick Start Guide
What You Will Need
  • VisualStudio Code
  • NPM
  • Node.js
  • Wijmo
  • Angular
Controls Referenced

PopUp

Tutorial Concept Efficiently integrate promise-based modal dialog functionality into your Angular applications, defining dynamic content and behavior through native Angular patterns within a single function call for clean asynchronous result handling.

Modal Dialogs in Web Applications

When it comes to selecting a style of modal dialog to use in your web application, you want something that is as easy to use as a native JavaScript dialog, such as the confirm function:

if(confirm('Do you agree?')) { do_it(); }

The problem with JavaScript's native alert/confirm/prompt functions is that they're very limited in terms of functionality and customization options. As an Angular developer, you likely value solutions that align seamlessly with the framework’s architecture and design patterns. Imagine building dialogs whose content, configuration, and behavior are defined entirely through idiomatic Angular constructs. Ideally, presenting a dialog would require nothing more than a single, well-structured method call, while handling user interaction and processing results asynchronously could be expressed through clean, maintainable code. Such an approach not only improves developer productivity but also ensures consistency, readability, and scalability across your application.

In this article, we’ll explore a streamlined approach to implementing a dialog function library that fulfills these goals. The proposed solution enables dialog content and behavior to be represented by an arbitrary Angular component, preserving flexibility and alignment with Angular’s component-driven architecture. Dialogs are invoked via a dedicated function that returns a result as a Promise object. We will explore:

Ready to get started? Download Wijmo Today!

JavaScript Promises

A Promise is an object that enables one segment of code awaiting the result of an asynchronous operation (referred to here as the Promise client) to coordinate with another segment of code responsible for producing that result (referred to as the Promise provider). For a more comprehensive explanation of JavaScript Promises, you can check out these additional resources.

The Promise provider creates the Promise object, and the Promise client can "subscribe" to this result using then().catch() methods:

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 determines that a result is available (e.g., after an XMLHttpRequest has completed), it invokes a resolution mechanism that transitions the Promise to a fulfilled state and triggers the execution of the Promise client's resultCallback, delivering the returned data. If the Promise provider detects an error or malfunction (e.g., an XMLHttpRequest has failed), it calls the rejectCallback with error data.

The value supplied to the resultCallback may itself be a Promise, enabling the composition of asynchronous workflows through chained .then().catch() calls:

promise.then(result1Callback).then(result2Callback).catch(reject1Callback).then(result3Callback).catch(reject2Callback);

Base Architecture

The Core showComponentInPopup Function

A low-level showComponentInPopup function is at the core of our dialog implementation. This function is capable of:

  • Creating an instance of any Angular component, initializing its properties with specific values, and rendering it in a (most likely modal) dialog window on screen.
  • Returning the outcome of the user’s interaction with a dialog as a Promise.

One example of implementation may look like:

showComponentInPopup(dialogWindowProperties, componentToShow, componentProperties).then(result => {/*process dialog result*/}).catch(error => {*/process error*/});

Specific Dialog Functions

With a showComponentInPopup function in place, we can create a library of specific dialog functions:

yesNoDialog('Do you accept?').then(() => /*do something if Yes*/).catch(() => /*do something if Not*/);

or

loginDialog(ownerElement).then((data) => /*process successful authorization*/).catch((error) => /*process authorization failure*/);

These are only a few examples of possible functions. For this example, we should:

  • Create a specific Angular component that represents the dialog content, contains the results of a user's dialog interaction, and potentially contains the results of the dialog's interaction with services.
  • Create a shortcut function that calls the showComponentInPopup function, then pass the component type and its initialization property values as the function parameters.

Implementation

showComponentInPopup Function

The showComponentInPopup function, as well as the library of shortcut functions, is implemented in our app/dialog.service.ts file. The code for this function is as follows:

import { Component, ComponentFactoryResolver, Injectable, ViewContainerRef, EventEmitter } from '@angular/core';

import * as wjcInput from '@mescius/wijmo.input';

export function showComponentInPopup(popupOptions: any, componentType: any, componentProperties: any, vcr: ViewContainerRef, cmpResolver: ComponentFactoryResolver): Promise<any> {
  // Create and initialize the 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 the 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();
  }).catch((error) => {
    popUp.hide();
  })

  return ret;
}

Now, we’ll examine this implementation in detail and review its core components.

Popup

Implementation requires a UI component capable of rendering content within a modal “window.” In this example, the Wijmo PopUp control serves as the dialog host. For more advanced interface scenarios, consider leveraging additional components such as our Angular datagrid and other comprehensive UI controls available in the suite.

You may alternatively use any modal implementation that aligns with your project requirements, such as a Bootstrap modal component. The first parameter of the showComponentInPopup function, popupOptions, accepts a configuration object that specifies Popup-related property values, enabling precise customization of the dialog’s behavior and appearance.

For example, the dialog can be configured to anchor to a specific DOM element rather than being displayed in the default centered position within the viewport. In this case, pass { owner: anchorElement } as the parameter value to instruct the Popup to visually stick to the anchorElement. The code below 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

Next, we’ll instantiate a component used as dialog content and initialize its properties. The function defines two parameters of the dialog content component: componentType, which accepts the component class (i.e., a reference to its constructor), and componentProperties, which allows a configuration object to be supplied for initializing the component’s input properties. In addition, the function definition includes two less immediately apparent parameters that warrant closer examination:

vcr: ViewContainerRef, cmpResolver: ComponentFactoryResolver

These parameters reference Angular services required to dynamically instantiate a component at runtime. They can be obtained via dependency injection by declaring them in the component's constructor that invokes the dialog function.

For a more in-depth explanation of dynamic component creation in Angular, refer to the official Angular documentation, which provides detailed guidance on the underlying concepts and recommended implementation.

These parameters translate into the following:

let cmpRef = vcr.createComponent(cmpResolver.resolveComponentFactory(componentType));

This code instantiates the component and assigns the resulting ComponentRef (which stores useful information about the component) to the cmpRef variable. The cmpRef.instance property references the instantiated component, while cmpRef.location.nativeElement retrieves the root DOM element representing the component's template. After creating the component, assign its properties using the values passed in the componentProperties parameter:

if(componentProperties){
  for(let prop in componentProperties) {
    cmp.instance[prop] = componentProperties[prop];
  }
}

Then, add the component’s DOM to the Popup so the popup displays the component’s template as its content:

popUp.content = cmpRef.location.nativeElement;

Finally, show the component on the screen:

popUp.show(false);

Promise as a Dialog

Any custom dialog implementation in JavaScript is inherently asynchronous. Unlike the native alert, confirm, or prompt functions, which block execution until the user responds, custom dialogs must operate without halting the main thread. Their outcomes become available at indeterminate times, driven by user interaction or, in more advanced scenarios, by additional asynchronous operations. For example, when a user submits credentials in a LoginDialog, the component may initiate an authentication request and await a server response. Only after that response is received can the dialog produce a definitive result.

Promises provide a modern and highly effective mechanism for managing such asynchronous workflows. When a function returns a Promise, the client code can add a .then().catch() chain of calls to the function, where the handler functions processing the results are passed as parameters. This pattern forms the foundation of the approach used in our implementation.

In this method, dialog results originate from the custom content components and reflect their specific semantics. The result is exposed as a component property of the Promise type. The showComponentInPopup function should identify and retrieve the Promise-based result from any supplied component, then return it as a function result. That is, we must sign a contract between the function and the content component that specifies how the function can retrieve the result from the component.

To formalize this contract, introduce the following interface:

export interface IPromiseResult {
  readonly result: Promise<any>;
}

Each dialog content component should implement the above interface, storing the dialog result as a Promise object returned by the result property. By executing this agreement, the component can reliably render the dialog result and return it as the function result:

let ret = (<IPromiseResult>cmpRef.instance).result;
return ret;

Now, the showComponentInPopup function must include one additional feature: automatically closing the popup once the dialog result is processed (i.e., the associated Promise is resolved or rejected). Relying on code to call the function would violate best practices and complicate the workflow, though.

To ensure proper system behavior, the function attaches its own .then().catch() handlers to the returned Promise, performing cleanup actions to hide the popup when the dialog completes.

Implementation of this logic is illustrated in the code below:

// The function's return value, assigned with a Promise representing the 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();
}).catch((error) => {
  popUp.hide();
})

return ret;

In this approach, the function adds its own .then().catch() chain to the returned Promise, while the function-calling application code adds a separate .then().catch() chain to the same Promise to process the dialog results. Multiple chains associated with the same Promise are not only valid, but fully supported by the Promise specification. This capability introduces Promise branching, which allows multiple asynchronous operations to run in parallel without interfering with one another.

The diagram below illustrates Promise Branching more simply:

Angular Promise Branching

Once the result is produced (i.e., the Promise is fulfilled or rejected), both registered .then().catch() chains are invoked, with the first attached chain executing prior to subsequent ones. Each chain operates independently, ensuring task separation: showComponentInPopup handles closing the popup, while the application-level chain performs additional processing according to business requirements.

Dialog Functions Library

With the showComponentInPopup function established, we can proceed to build a dialog function library that leverages this capability. The library is represented by the DialogService class, which contains static methods and is defined within the app/dialog.service.ts file.

Confirmation Dialogs

This implementation serves as a functional equivalent of the native confirm function, presenting a dialog that displays a confirmation message alongside two action buttons (OK and Cancel), allowing the user to either proceed or cancel the operation. To accommodate different contexts, we will provide two variants: okCancelDialog, featuring OK and Cancel actions, and yesNoDialog, offering Yes and No options. In both cases, the confirmation message is supplied as a parameter to the function. The initial step in building such a dialog function is to create a dedicated component that encapsulates the dialog’s content.

To achieve this, define the OkCancelCmp component within the app/dialog.service.ts file as shown below:

// OkCancelCmp component implements content of the Dialogs.yesNoDialog dialog.
@Component({
  selector: 'ok-cancel-cmp',
  template:
    `<div style="padding:10px 10px">
      {{question}}
      <br/>
      <br/>
      <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 message and button labels are defined by the question, okCaption, and cancelCaption properties, with the latter two defaulting to "OK" and "Cancel" respectively. These defaults can be overridden by assigning alternative values as needed. In accordance with the contract defined by the IPromiseResult interface, the dialog outcome is exposed through a result property of the Promise type. Given that this dialog supports only two possible outcomes (confirmation or cancellation), it is natural to model confirmation as a fulfilled Promise and cancellation as a rejected Promise, reflecting the user’s selection in a clear and meaningful way.

An application integrating this dialog functionality might resemble the following:

okCancelDialog('Do this?').then(okDelegate).catch(cancelDelegate);

The okDelegate function is called when the user selects the “OK” button, while cancelDelegate executes if “Cancel” is chosen. The Promise stored in the result property is instantiated within the component’s constructor, establishing the mechanism through which the dialog outcome is communicated, as shown below:

constructor() {
  this._result = new Promise((resolve, reject) => {
    this._resolve = resolve;
    this._reject = reject;
  });
}

The Promise constructor accepts an executor function with two parameters: resolve and reject. These parameters are callback references supplied by the client code through the .then() and .catch() handlers. In the context of the previous example, okDelegate is effectively passed as the fulfillment handler (via resolve), while cancelDelegate is associated with the rejection handler (via reject). As the provider of the Promise object, the component assumes responsibility for invoking the appropriate callback based on user interaction:

  • Call resolve when the Promise result is available (i.e., when the user selects “OK”) thereby triggering the delegate passed by the client code in the .then() call.
  • Call reject when the Promise is rejected (i.e., when the user selects “Cancel”), which executes the delegate passed by the client code in the .catch() call.

To facilitate this, the resolve and reject parameter values are stored in the component's private properties (_resolve and _reject, respectively).

A click event handler is then implemented and bound to the template’s buttons to invoke the appropriate callback based on the user’s selection, as shown below:

<button (click)="clicked(true)">{{okCaption}}</button>
<button (click)="clicked(false)">{{cancelCaption}}</button>

In this method, _resolve or _reject is invoked based on the supplied parameter, thereby fulfilling or rejecting the Promise in accordance with the user’s action:

clicked(ok: boolean) {
  if(ok) {
    this._resolve();
  } else {
    this._reject();
  }
}

With the dialog content component fully implemented, the foundation is complete. We can now proceed to define the corresponding dialog functions with minimal additional effort:

export class DialogService {
  static okCancelDialog(question: string, vcr: ViewContainerRef, cmpResolver: ComponentFactoryResolver): Promise<any> {
    return showComponentInPopup({}, OkCancelCmp, { question: question }, vcr, cmpResolver);
  } 
}

The function calls showComponentInPopup, supplying the content component type (OkCancelCmp) as a parameter. It also passes the component property values, specifically the question property value in this scenario, whose value is passed directly from the function’s corresponding parameter:

{ question: question }

It also passes the previously discussed ViewContainerRef and ComponentFactoryResolver objects, which are supplied to the function as parameters:

static yesNoDialog(question: string, vcr: ViewContainerRef, cmpResolver: ComponentFactoryResolver): Promise<any> {
  return showComponentInPopup({}, OkCancelCmp, { question: question, okCaption: 'Yes', cancelCaption: 'No' }, vcr, cmpResolver);
}

This function represents a variation of okCancelDialog, differing only in the button labels, which are set to Yes and No. In this case, alongside the question property value, the custom button caption values are also passed to the content component:

Making the Library an NgModule

Dynamically created Angular components introduce an important consideration. Any component created by the ViewContainerRef.createComponent method should be added appropriately to an NgModule's entryComponents metadata property. To address this requirement, define a dedicated DialogsModule within the app/dialog.service.ts file. This module declares and references all components intended for use as dialog content within the dialog function library. Its definition is shown below:

@NgModule({
  imports: [BrowserModule, WjInputModule],
  declarations: [LoginCmp, OkCancelCmp],
  entryComponents: [LoginCmp, OkCancelCmp],
})
export class DialogsModule {
}

With this NgModule in place, any application module that leverages the library simply needs to import DialogsModule within its own NgModule definition.

Using the Confirmation Dialog Functions in an Application

We will now leverage the functionality defined in app/data.service.ts to implement Promise-based dialogs in the app.component.ts file. The first step is to import the service into the application’s AppModule:

import { Component, ComponentFactoryResolver, Inject, ViewContainerRef } from '@angular/core';
import { DialogService } from './dialog.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  ...
}

Next, obtain references to the ViewContainerRef and ComponentFactoryResolver services, as previously discussed. These are acquired through Angular’s injection mechanism by declaring them as parameters in the component’s constructor:

constructor(@Inject(ViewContainerRef) private _vcr: ViewContainerRef, @Inject(ComponentFactoryResolver) private _cmpResolver: ComponentFactoryResolver) {}

Now, these are ready for use. Add the following code under the constructor:

yesNoDialog() {
  DialogService.yesNoDialog('Do you really want it?', this._vcr, this._cmpResolver).then(() => alert(`You answered YES`)).catch(() => alert(`You answered NO`));
}

This will display an alert "You answered YES" when the user selects “Yes,” and "You answered NO" when “No” is chosen.

okCancelDialog() {
  DialogService.yesNoDialog('Do you confirm this action?', this._vcr, this._cmpResolver).then(() => alert(`You confirmed the action`)).catch(() => alert(`You cancelled the action`));
}

This will display an alert "You confirmed the action" when users select “OK,” and "You cancelled the action" if “Cancel” is selected.

Login Dialog Simulator

For the sake of simplicity, this example does not represent a production-ready login dialog, but rather a simplified simulation designed to demonstrate the core interaction pattern. Specifically, when the user confirms the dialog action, a request is sent to a server, and based on the authentication results (success or failure), the Promise representing the dialog result is either resolved or rejected.

To emulate this behavior, the simulated login dialog provides two buttons: Simulate Success and Simulate Failure. The dialog content is implemented by the LoginCmp component defined in the dialog.service.ts file as shown below:

@Component({
  selector: 'login-cmp',
  template:
    `<div style="padding:10px 10px">
      <b>Login Dialog</b>
      <br/>
      <br/>
      <button (click)="simulate(true)">Simulate Success</button>
      <br/>
      <br/>
      <button (click)="simulate(false)">Simulate Failure</button>
    </div>`
})
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);
  }
}

The login dialog employs a slightly different technique for resolving or rejecting the resulting promise. In this case, the component exposes an onResult event to communicate the outcome:

onResult = new EventEmiter();

This event is emitted once the authentication results are received from a server. The event value includes the authentication results, represented here as a simple success/failure boolean value. The event is triggered in the simulate method, which uses a timeout to mimic asynchronous server communication. The result Promise is created in the constructor, while the Promise callback function subscribes to the onResult event, either resolving or rejecting it based on the event data value. In the case of success, the Promise is resolved with a "Done" string; in the case of failure, it is rejected with an "Access denied" error. These values can then be inspected by the code that calls the dialog. With the content component complete, the final step is to create the corresponding dialog function, loginDialog:

static loginDialog(anchor: HTMLElement, vcr: ViewContainerRef, cmpResolver: ComponentFactoryResolver): Promise<any> {
  return showComponentInPopup({ owner: anchor }, LoginCmp, {}, vcr, cmpResolver);
}

Unlike the okCancelDialog function, which presents the dialog centered within the viewport, suppose the loginDialog needs to be visually anchored to a specific UI element, such as the button that initiated the dialog. This element is specified in the anchor parameter and is assigned to the Popup’s owner property. The configuration is applied by passing a Popup options object as the first parameter of the showComponentInPopup function, enabling precise control over the dialog’s positioning behavior:

{ owner: anchor }

The function is now ready for use in the application. Add the following code to the app.component.html file:

<button #button1 (click)="loginDialog(button1)">Login</button>

Then, add the code below to the app.component.ts file:

loginDialog(owner: HTMLElement) {
  DialogService.loginDialog(owner, this._vcr, this._cmpResolver).then((data) => alert(`Logged in! Status: ${data}`)).catch((error) => alert(`Login failed. Error: ${error}`));
}

In this example, the button1 element is assigned as the owner of the dialog, ensuring that the popup is visually anchored to the button rather than displayed in the default centered position. The .then() handler function uses the data parameter containing a resolved Promise's data, while the .catch() handler function uses the error parameter that provides error details of the rejected Promise.

Implementing Modal Dialogs

This article explored one practical approach to implementing function-driven modal dialogs in Angular, where both content and behavior are defined by arbitrary Angular components and dialog results are represented by Promise objects. Access this blog’s sample code on StackBlitz.

The examples above leveraged Wijmo's Popup control. For more advanced interface requirements, explore additional components such as our Angular DataGrid and other high-performance UI controls available within Wijmo’s JavaScript UI library.

Ready to try it out? Download Wijmo Today!

comments powered by Disqus