User interface
Angular CSS HTML Material RxJS SPA Typescript Visual Studio Code

How to use Modal Material Dialogs in Angular Applications

Welcome to today’s post.

In today’s post I will show how to use Modal Material Dialogs within an Angular application.

Dialogs are a user interface widget that allows us to receive data from a user, then process that data. The data is usually a set of properties that are used to configure the settings within an application feature such as a set of reports, a search or a wizard that has a set of default settings that can be overridden by the user to produce a more desired result or enable specific sub-features.

You may also refer to the Angular Material Dialog library site where there is an overview with some basic examples.

The above examples show a simple example of a dialog that shows a label and the text control, without showing data persistence.

An Application Settings Dialog

I will show an example of a dialog that is used quite often in web applications to present a set of application settings to the user within a dialog, then store these changes within the component for later use.

I will show the dialog component, then explain the source code.

In the HTML template for a Material Dialog, we have the basic structure, which consists of content and actions:

<div mat-dialog-content>
    ...
   	<mat-dialog-actions>
        <button mat-button 
            [mat-dialog-close]="true" cdkFocusInitial>
            Apply
        </button>

        <button mat-button mat-dialog-close>
            Cancel
        </button>
    </mat-dialog-actions>
</div>

When we implement the content, which includes the form input controls, such as <input> elements that are bound to the data model, data, the template looks as follows: 

<h1 mat-dialog-title>User Settings</h1>
<div mat-dialog-content *ngIf="data">
    <form>        
        <div class="form-group" style="height: 10px;">
        </div>
        <div class="form-group">
            <label for="location">Notification Interval (minutes):</label>
            <input [(ngModel)]="data.notificationInterval" 
                name="notificationInterval" 
                class="form-control col-sm-6" 
                placeholder="Notification Interval" 
                placement="right" 
                ngbTooltip="Notification Interval">
        </div>

        <div class="form-group">
            <label for="location">Search Record Set Size</label>

            <input [(ngModel)]="data.resultRecordSetSize" 
                name="resultRecordSetSize" 
                class="form-control col-sm-6" 
                placeholder="Search Record Set Size" 
                placement="right" 
                ngbTooltip="Search Record Set Size">
        </div>
    </form>

    <mat-dialog-actions>
        <button mat-button 
            [mat-dialog-close]="true" cdkFocusInitial>
            Apply
        </button>

      	<button mat-button mat-dialog-close>
            Cancel
        </button>
    </mat-dialog-actions>
</div>

The attribute cdkFocusControl is used the set the initial control focus on the dialog when it is first opened. The attribute [mat-dialog-close] is the result that returns in the button action. In the above button labelled Apply, the result returned is true, and the result for the button labelled Cancel is blank (no value).

The typescript code of the dialog Component is shown below:

import { Component, Inject, OnInit } from '@angular/core';
import { MAT_DIALOG_DATA } from '@angular/material/dialog';

export interface DialogData {
    notificationInterval: number;
    resultRecordSetSize: number;
}

@Component({
    selector: 'app-user-settings-dialog',
    templateUrl: './user-settings-dialog.component.html',
    styleUrls: ['./user-settings-dialog.component.scss']
})
export class UserSettingsDialogComponent implements OnInit {

    settingsForm: any;

    constructor(@Inject(MAT_DIALOG_DATA) public data: DialogData) {}

    ngOnInit(): void {
        console.log("opening user-settings-dialog...");
    }
}

In the next section, I will explain how dialog data is used in the modal dialog.

Explaining Dialog Data

In the previous section I injected dialog data into the user settings component. I will explain how it is used to transfer modified dialog properties to other components.

I have defined the dialog data that represents the application settings within an interface as shown:

export interface DialogData {
    notificationInterval: number;
    resultRecordSetSize: number;
}

The settings are injected within the dialog through the constructor’s data parameter as an injection token, MAT_DIALOG_DATA:

constructor(@Inject(MAT_DIALOG_DATA) public data: DialogData) {}

The data variable is then accessible within the component source and the HTML template for the dialog. In addition, we can tweak the CSS for the dialog component’s mat-dialog-content style by hiding the overflow to hide the scroll bar and adjusting the padding:

.mat-dialog-content {
    padding: 0 24px;
    overflow: hidden;    
}

We next require a helper component that does the following:

  1. Opens the dialog.
  2. Passes settings input object into the dialog’s data input parameter. 
  3. Retrieves the result including any changes to the data input. 

The helper component HTML is quite basic and allows us to conduct a basic manual test of the dialog component.

We could also integrate the dialog to a menu item or a link within one of our screens.

The user-settings-helper component HTML is shown below:

<p>User-settings-helper Demo</p>

<button mat-button (click)="openDialog()">Open dialog</button>

The component source TypeScript implementation is shown below:

import { Component, OnInit } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { DialogData, UserSettingsDialogComponent } 
from '../user-settings-dialog/user-settings-dialog.component';
import * as cloneDeep from 'lodash/cloneDeep';

@Component({
    selector: 'user-settings-helper',
    templateUrl: './user-settings-helper.component.html',
    styleUrls: ['./user-settings-helper.component.scss']
})
export class UserSettingsHelperComponent implements OnInit {
    defaultData: DialogData;
    modifiedData: DialogData = { 
    notificationInterval: 5, 
    resultRecordSetSize: 10 
  };

  constructor(public dialog: MatDialog) {
      this.defaultData = cloneDeep(this.modifiedData);
  }

  openDialog() {
      const dlgRef = this.dialog.open(UserSettingsDialogComponent, 
      {
          data: this.modifiedData,
          height: '280px',
          width: '260px'
      });

      dlgRef.afterClosed().subscribe(result => {
   	      console.log(`Dialog result: ${result}`);
          if (!result)
      	      this.modifiedData = cloneDeep(this.defaultData);
          this.defaultData = cloneDeep(this.modifiedData); 
          console.log("modified(notificationInterval) = " + 
          this.modifiedData.notificationInterval);
          console.log("modified(resultRecordSetSize) = " + 
          this.modifiedData.resultRecordSetSize);  
          console.log("default(notificationInterval) = " + 
          this.defaultData.notificationInterval);
          console.log("default(resultRecordSetSize) = " + 
          this.defaultData.resultRecordSetSize);  
    });
  }

  ngOnInit(): void {
      console.log("opening user settings helper..");
  }
}

The helper includes a constructor that takes the Material Dialog instance as an input:

constructor(public dialog: MatDialog) 

Processing Inputs and Outputs from Opening and Closing a Dialog

The openDialog() method opens the instance to the UserSettingsDialogComponent dialog component settings dialog with specified width and height properties, along with the default data object for the settings of type DialogData that is initialised within the constructor.

const dlgRef = this.dialog.open(UserSettingsDialogComponent, 
{
    data: this.modifiedData,
    height: '280px',
    width: '260px'
});

The dialog’s afterClosed() event handler is then implemented to retrieve the dialog result and the state of the dialog data:

dlgRef.afterClosed().subscribe(result => {
   	console.log(`Dialog result: ${result}`);
        if (!result)
            this.modifiedData = cloneDeep(this.defaultData);
        this.defaultData = cloneDeep(this.modifiedData); 
    …
});

The above handler resets the dialog input data settings, modifedData to the default settings data defaultData, if the user selected the Cancel button. The default settings data is then set to a copy of the modified settings data, so that in the event of the dialog changes being cancelled, they will reinstate to the dialog input data.

Note that I used the lodash library function cloneDeep() to allow us to clone the source object, removing the references of the source object before they are assigned to the destination object. If we did not clone the modified settings data, then we would reference the default data and would not be able to reset modified settings data back to original settings when the dialog is cancelled.

To install and use the lodash object deep clone Node.js function, which is at the following NPM site and and at the Lodash site.

We use the following NPM command within the Visual Studio Code terminal:

npm i --save lodash.clonedeep

Once installed, your package.json will include the reference to the lodash library:

 "lodash.clonedeep": "^4.5.0",

Importing the deep clone function within a component then requires the following import:

import * as cloneDeep from 'lodash/cloneDeep';

In the next section, I will show how to configure and import the model dialog within the Angular application configuration module.

Configuring the Dialog Component within the Angular Application Module

Before we can run the above dialog implementation, we will need to include the dialog component within the component module’s entryComponents section:

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MaterialModule } from '../material/material.module';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';

import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { UserSettingsHelperComponent } from './user-settings-helper/user-settings-helper.component';
import { ConfigurationRoutingModule } from './configuration-routing.module';
import { MatDialogModule } from '@angular/material/dialog';
import { UserSettingsDialogComponent } from './user-settings-dialog/user-settings-dialog.component';

@NgModule({
    declarations: [
        UserSettingsHelperComponent,
        UserSettingsDialogComponent
    ],
    entryComponents: [
        UserSettingsDialogComponent
    ],        
    imports: [
        CommonModule,
        MaterialModule,
        NgbModule,
        FormsModule,
        ReactiveFormsModule,
        MatDialogModule,
        ConfigurationRoutingModule
    ],
    providers: [
    ]
})

export class ConfigurationModule { }

If we do not include the dialog component within EntryComponents, then we will end up with the following error when the dialog is activated:

“No component factory found for UserSettingsDialogComponent. Did you add it to @NgModule.entryComponents?”

And

“Cannot read property ‘focusInitialElementWhenReady’ of undefined”

A screen shot of the error within Chrome developer debugger tools is shown:

As is stated within the Material dialog reference, you will need to include the dialog component within entryComponents if the Angular project uses the ViewEngine. If the project uses Angular Ivy, then entryComponents is not required.

In the next section, I will cover some scenarios when testing the modal dialog.

A Test Run for the Modal Dialog

When our component is activated, the default settings populate the dialog input fields:

If the above dialog is cancelled, the settings properties will output to the console. I will discuss three cases:

Case 1: Cancel Unmodified Settings

The debug output is shown below:

Dialog result: 
modified(notificationInterval) = 5
modified(resultRecordSetSize) = 10
default(notificationInterval) = 5
default(resultRecordSetSize) = 10

When the default settings are modified and applied/accepted as shown below:

The console debug output is as shown:

Case 2: Apply Modified Settings

The debug output is shown below:

Dialog result: true
modified(notificationInterval) = 8
modified(resultRecordSetSize) = 12
default(notificationInterval) = 8
default(resultRecordSetSize) = 12

When the settings are subsequently cancelled, the console debug is shown below:

Case 3: Cancel Modified Settings

The debug output is shown below:

Dialog result: true
modified(notificationInterval) = 8
modified(resultRecordSetSize) = 12
default(notificationInterval) = 8
default(resultRecordSetSize) = 12

A screen shot of the property values is shown below:

As we have seen, the Angular Material Dialog component is quite a straightforward and a useful component to use within an application especially where we give users the option of updating application settings with dialog data sharing.

That is all for today’s post.

I hope you have found this post useful and informative.

Social media & sharing icons powered by UltimatelySocial