Message Box in Angular

Coming from a Windows application background, I feel the need of having a message prompt for scenarios such as confirmation or displaying information. In web, there are quite a number of ways to show a message popups – each of them have their own pros and cons.

The message box control that is discussed in this article uses Material Dialog and shows a popup until the user acknowledges the message box, similar to a Windows message box. This might appear intrusive for users as they would have to scroll into the message box in order to acknowledge the message and this might not be mobile friendly. But, for now, the target for this message box are the desktop users. We will work into making this control responsive in the future.

This message box control will be part of the reusable UI component library that I am currently building. As of this writing, I have just built the library in an Angular workspace with a demo app. I’ll make it available in npm registry soon. For now, you can clone or download and include the library from GitHub.

Here, I’m using Visual Studio code to prevent unnecessary creation of a Visual Studio project file which would compel us to select a template such as ASP.NET Core Project. The creation of Angular application and library is similar as before using ng new and ng library commands.

In this article, I will start discussing on how to use the library to your application. Then, I will discuss the inner workings of the message box control. If you don’t want to know how the message box works, you can skip that section.

Message Box Usage

Step 1: Import the TSC UI Module

Go to your app.module and import TscUiModule:

// other imports
import { TscUiModule } from 'tsc-ui';

@NgModule({
  // other attributes
  imports: [
    // more imports
    TscUiModule,
  ],
})
export class AppModule { }

Step 2: Import and Inject the Message Box Service

In you component, import the MessageBoxService and inject it in the constructor:

import { MessageBoxService } from 'tsc-ui';

@Component({
  selector: 'app-message-box-demo',
  templateUrl: './message-box-demo.component.html',
  styleUrls: ['./message-box-demo.component.css']
})
export class MessageBoxDemoComponent implements OnInit {
  
  constructor(private messageBox: MessageBoxService) { }
  
}

Step 3: Show the Message Box

In any of your methods, show the message box as needed:

showBasic() {
  this.messageBox.show("Basic Usage", "This is a basic message box with title and message.");
}

Sample Output:

API Reference

MessageBox Parameters

Parameter NameTypeDescription
titlestringRequired. The text to be displayed on the title bar
messagestringRequired. The text to be displayed in the message box
buttonsMessageBoxButtonsOptional. The buttons to be displayed in the message box. Default: ok.
resultCallback(result: MessageBoxResult) => voidOptional. The method called after the user acknowledges the message box.

MessageBoxButtons Enum

Enum ValueDescription
okDisplays an OK button.
okCancelDisplays OK and Cancel buttons.
abortRetryIgnoreDisplays Abort, Retry, and Ignore buttons.
yesNoCancelDisplays Yes, No, and Cancel buttons.
yesNoDisplays Yes and No buttons.
retryCancelDisplays Retry and Cancel buttons.

MessageBoxResult Enum

Enum ValueDescription
noneInitial value when the user haven’t clicked any button yet.
okValue when the user clicked on OK button.
cancelValue when the user clicked on Cancel button.
abortValue when the user clicked on Abort button.
retryValue when the user clicked on Retry button.
ignoreValue when the user clicked on Ignore button.
yesValue when the user clicked on Yes button.
noValue when the user clicked on No button.

Implementation

As mentioned earlier, the message box control is built on top of Material Dialog, so if you haven’t done yet, install Angular material on the library.

The message box is a dialog with configurable title, message, and buttons as well as a callback once the user acknowledges the message box. As such, we build our message box the same way as any dialog. Let’s build, then, the skeleton of our MessageBoxComponent:

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

@Component({
  selector: 'tsc-message-box',
  templateUrl: './message-box.component.html',
  styleUrls: ['./message-box.component.scss']
})
export class MessageBoxComponent implements OnInit {
  messageBoxResult = MessageBoxResult;

  title: string;
  message: string;

  constructor(
    public dialogRef: MatDialogRef<MessageBoxComponent>,
    @Inject(MAT_DIALOG_DATA) public data: any
  ) { 
    this.title = data.title;
    this.message = data.message;
  }

  ngOnInit() {
    this.dialogRef.disableClose = true;
  }

  onAcknowledge() {
    this.dialogRef.close();
  }
}

We keep things simple for now – we just have a title and a message property. In our constructor, we inject MatDialogRef<MessageBoxComponent> to allow us to do some configuration and to allow us to close the dialog when user acknowledges the message box. We also inject a MAT_DIALOG_DATA passed when showing our dialog. Here, our data is not strongly-typed for simplicity’s sake. We’ll make it strongly typed later.

Then, our template:

<div class="message-box">
  <h2 mat-dialog-title cdkDrag cdkDragRootElement=".cdk-overlay-pane" cdkDragHandle>
    {{title}}
  </h2>

  <div mat-dialog-content>
    <p>{{message}}</p>
  </div>

  <div mat-dialog-actions>
    <button mat-raised-button color="primary" (click)="onAcknowledge()">OK</button>
</div>

Pretty straightforward here, we have a title, message and an acknowledgement button. Let’s put a bit of a style:

::ng-deep .mat-dialog-container {
    padding: 0;
    overflow: hidden;

    .message-box {
        .mat-dialog-title {
            padding: 8px 12px;
            color: #fff;
            background: #102598;
            border-bottom: 1px solid #e5e5e5;
            font-size: 10pt;
            font-weight: bold;
        }
    
        .mat-dialog-content {
            padding: 0 36px;
            min-width: 360px;
            font-size: 10pt;
            color: #000;
        }
    
        .mat-dialog-actions {
            padding: 8px 12px 32px 12px;
            border-top: 1px solid #e5e5e5;
            display: block;
            text-align: right;
        }
    
        .mat-dialog-actions button {
            font-size: 9pt;
            font-weight: bold;
            color: #fff;
            background: #102598;
            margin: 3px 2px;
            padding: 0;
            outline: 0;
        }
    }
}

Here, we have overridden the themes being used so we may have a mismatch with your themes. We will cover theming on a future post so for now, let’s stick to the sample.

Next, we create our MessageBoxService that will allow us to show our message box:

import { Injectable } from '@angular/core';
import { MatDialog } from '@angular/material';

import { MessageBoxComponent } from '../components/message-box/message-box.component';

@Injectable({
  providedIn: 'root'
})
export class MessageBoxService {

  constructor(private dialog: MatDialog) { }

  show(
    title: string, 
    message: string): void {
    
    this.dialog.open(MessageBoxComponent, { data: { title: title, message: message }});
  }
  
}

In our MessageBoxService, we inject a MatDialog, which allows us to show the message box by calling its open method passing the MessageBoxComponent type and our data.

Since we are dynamically instantiating a component during runtime via TypeScript, we have to add our MessageBoxComponent into the entryComponent in our library module. Our library module should look like:

import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';

import { AppUiModule } from './app-ui.module';

import { MessageBoxComponent } from './components/message-box/message-box.component';

@NgModule({
  declarations: [ MessageBoxComponent] ,
  imports: [ CommonModule, AppUiModule ],
  exports: [ MessageBoxComponent ],
  entryComponents: [ MessageBoxComponent ]
})
export class TscUiModule { }

Note here that we have created an AppUiModule used to import UI related modules such as Angular Material:

import { NgModule } from '@angular/core';
import { DragDropModule } from '@angular/cdk/drag-drop';
import { MatButtonModule, MatDialogModule } from '@angular/material';

@NgModule({
  declarations: [],
  imports: [ 
    DragDropModule,
    MatButtonModule,
    MatDialogModule
  ],
  exports: [ 
    DragDropModule,
    MatButtonModule,
    MatDialogModule
  ]
})
export class AppUiModule { }

We used three Material components and behavior:

  • MatDialogModule – for our MessageBoxComponent, which is a dialog
  • MatButtonModule – for our acknowledgement button
  • DragDropModule – to allow a movable message box by dragging on the drag handles, which is on our title bar

After building our library, you can use the message box by following the steps specified in the Message Box Usage section.

Now, let’s define the allowable buttons that our library can provide. We create a MessageBoxButtons enum:

export enum MessageBoxButtons {
    ok,
    okCancel,
    abortRetryIgnore,
    yesNoCancel,
    yesNo,
    retryCancel
}

Let’s also define the MessageBoxResult enum as the button selected or clicked by the user:

export enum MessageBoxResult {
    none,
    ok,
    cancel,
    abort,
    retry,
    ignore,
    yes,
    no
}

We modify our component adding the buttons and resultCallback properties. We also expose the MessageBoxResult enum as a property since we will be using it in our html template:

import { MessageBoxButtons } from '../../models/message-box-buttons.enum';
import { MessageBoxResult } from '../../models/message-box-result.enum';

@Component({
  selector: 'tsc-message-box',
  templateUrl: './message-box.component.html',
  styleUrls: ['./message-box.component.scss']
})
export class MessageBoxComponent implements OnInit {
  messageBoxResult = MessageBoxResult;

  title: string;
  message: string;
  buttons: MessageBoxButtons;
  resultCallback: (result: MessageBoxResult) => void;

  // more code here...
}

Let’s also create a strongly-typed model for the data being passed to our message box:

import { MessageBoxButtons } from './message-box-buttons.enum';
import { MessageBoxResult } from './message-box-result.enum';

export class MessageBoxModel {
    constructor(public title: string, public message: string, public buttons: MessageBoxButtons, public resultCallback: (result: MessageBoxResult) => void) { }
}

Then, modify our MessageBoxComponent constructor to accept our strongly-typed data instead of an anonymous type:

constructor(
  public dialogRef: MatDialogRef<MessageBoxComponent>,
  @Inject(MAT_DIALOG_DATA) public data: MessageBoxModel
) { 
  this.title = data.title;
  this.message = data.message;
  this.buttons = data.buttons;
  this.resultCallback = data.resultCallback;
}

Now, in order to simplify the display of the buttons in our html template, we create helper methods to determine if a particular button is visible or not. For example, for checking if the OK button is visible or not:

isOkVisible(): boolean {
  return this.buttons == MessageBoxButtons.ok || this.buttons == MessageBoxButtons.okCancel;
}

It might seem a lot of code but, believe me, this would simplify our html template logic.

Now, we modify our onAcknowledge method of our MessageBoxComponent to handle the resultCallback:

onAcknowledge(result: MessageBoxResult) {
  if(this.resultCallback) this.resultCallback(result);
  this.dialogRef.close();
}

Now, we modify our html template to add all the possible buttons and invoke the onAcknowledge() method during their click event passing the appropriate MessageBoxResult:

<div class="message-box">
  <h2 mat-dialog-title cdkDrag cdkDragRootElement=".cdk-overlay-pane" cdkDragHandle>
    {{title}}
  </h2>

  <div mat-dialog-content>
    <p>{{message}}</p>
  </div>

  <div mat-dialog-actions>
    <button mat-raised-button color="primary" (click)="onAcknowledge(messageBoxResult.ok)" *ngIf="isOkVisible()">OK</button>
    <button mat-raised-button color="primary" (click)="onAcknowledge(messageBoxResult.abort)" *ngIf="isAbortVisible()">Abort</button>
    <button mat-raised-button color="primary" (click)="onAcknowledge(messageBoxResult.retry)" *ngIf="isRetryVisible()">Retry</button>
    <button mat-raised-button color="primary" (click)="onAcknowledge(messageBoxResult.ignore)" *ngIf="isIgnoreVisible()">Ignore</button>
    <button mat-raised-button color="primary" (click)="onAcknowledge(messageBoxResult.yes)" *ngIf="isYesVisible()">Yes</button>
    <button mat-raised-button color="primary" (click)="onAcknowledge(messageBoxResult.no)" *ngIf="isNoVisible()">No</button>
    <button mat-raised-button color="primary" (click)="onAcknowledge(messageBoxResult.cancel)" *ngIf="isCancelVisible()">Cancel</button>
  </div>
</div>

Then, let’s modify the show() implementation of our service to add support to these two additional parameters:

show(
  title: string, 
  message: string, 
  buttons: MessageBoxButtons = MessageBoxButtons.ok, 
  resultCallback: ((result: MessageBoxResult) => void) = undefined): void {
    
  this.dialog.open(MessageBoxComponent, { data: new MessageBoxModel(title, message, buttons, resultCallback) });
}

A sample usage for our message box with buttons and resultCallback parameters:

this.messageBox.show("Callback", "This is a message box showing Yes and No buttons with callback.", MessageBoxButtons.yesNo, (result) => {
  this.messageBox.show("Result", "Your choice: " + (result == MessageBoxResult.yes ? "Yes" : "No"));
});

Sample Output:

This is the initial implementation of the message box control. I will make a few more improvements and refinements before I make it available in the npm registry.