Let's face it: sometimes your Angular app can take either "yes" or "no" for an answer. That's why you need a reusable confirmation dialog component.

In this guide, I'll show you how to use Angular Material to create a dialog box that's suitable for a variety of purposes. You'll find yourself calling on it repeatedly.

As is usually the case, though, you could go straight to the code on GitHub. 

Remember: if you're having problems running the Angular app locally, be sure to check the README.

Please note: this guide is a part of an ongoing series of tutorials on how to create a customer relationship management (CRM) application from scratch. The GitHub code for each guide belongs to a specific branch. The master branch holds the entire application up to the current date. If you want to follow the whole series, just view the careydevelopmentcrm tag. Note that the website displays the guides in reverse chronological order so if you want to start from the beginning, go to the last page.

The Business Requirements

During your lunch break, your boss Smithers waltzes into your office and tells you he noticed a problem with that CRM app you're working on.

"People can accidentally sign out!" he yells.

And, indeed, he's right. If you click on the Sign Out menu item you created in the last guide, you'll see that it just logs users out without asking them if they really want to logout.

"We can't have that!" Smithers says, exasperated. "You must put a confirmation dialog in there! Yesterday!"

He leaves your office huffing.

Starting a Dialog

First, you need to create a confirmation dialog that's reusable throughout the app. Start by going to the command line at the root of your app source and enter the following command:

ng g c ui/confirmation-dialog

That should create a new folder call ui in src/app. Inside the ui folder, you should see a confirmation-dialog folder. Inside that folder, you'll see a few new files.

But before you can edit those files, you need to create a new one. Within that confirmation-dialog folder, create a file called confirmation-dialog.ts.

Then, add this code:

export class ConfirmationDialogModel {
    constructor(public title: string, public message: string) { }
}

That's nothing more than a simple model that gives you access to both the title and message of the confirmation dialog.

Next, edit confirmation-dialog.component.ts. Make it look like this:

import { Component, Inject } from '@angular/core';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { ConfirmationDialogModel } from './confirmation-dialog';

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

    title: string;
    message: string;

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

    onConfirm(): void {
        this.dialogRef.close(true);
    }

    onDismiss(): void {
        this.dialogRef.close(false);
    }
}

The imports at the top are probably new to you.

The MatDialogRef import is used to reference the dialog itself once opened. 

The MAT_DIALOG_DATA import probably took you by surprise. What is that and why is it in all upper case?

You won't see that too much in Angular. It's an injection token that you can use to access data passed into the dialog.

And what data will you pass in? That model you just created.

The component class, like the model, includes a handle on both the title and the message in the dialog. In fact, both of those fields are populated from an instance of that ConfirmationDialogModel class.

Speaking of that, take a look at the constructor. You might find that a little unusual as well.

For starters, the first parameter is an instance of the aforementioned MatDialogRef. It's stereotyped to this exact class.

Why? Because this is the component that supports the dialog.

Now what the heck is that @Inject business doing in the constructor?

That's called a parameter decorator. In this case, you're telling Angular to make data  a singleton associated with MAT_DIALOG_DATA.

Keep in mind: data here is an instance of that ConfirmationDialogModel class. So that's the object associated with the token.

The two methods in the class simply dictate how to behave when the user closes or dismisses the dialog.

Next, update the HTML associated with this component by editing confirmation-dialog.component.html:

<h4 mat-dialog-title>
  {{title}}
</h4>

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

<div mat-dialog-actions>
  <button mat-raised-button cdkFocusInitial (click)="onDismiss()">No</button>
  <button mat-raised-button (click)="onConfirm()">Yes</button>
</div>

Nothing too complicated there.

For starters, you set the title of the dialog to the title property defined in the class you just updated. And you do the same thing with the message property as well.

Finally, the last section creates two buttons: Yes and No. Those are fairly self-explanatory and they fire the two methods you saw in the class above.

Getting to the Root of the Matter

Next up, you need to add UI support for that confirmation dialog way down deep in your app module. 

In Microsoft Visual Studio, navigate to src/app/app.module.ts.

Add the following lines in the import section:

import { MatDialogModule } from '@angular/material/dialog';
import { MatButtonModule } from '@angular/material/button';
import { ConfirmationDialogComponent } from './ui/confirmation-dialog/confirmation-dialog.component';

Why in app.module.ts? Because, like many other Angular Material components, you're likely to use the dialog in various modules throughout the app. 

You'll also need to add ConfirmationDialogComponent to the declarations array and the other two modules to the imports array.

So why don't I just make your life easy and give you the whole thing:

import { BrowserModule } from '@angular/platform-browser';
import { FlexLayoutModule } from '@angular/flex-layout';
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatSidenavModule } from '@angular/material/sidenav';
import { MatListModule } from '@angular/material/list';
import { MatIconModule } from '@angular/material/icon';
import { MenuListItemComponent } from './features/ui/menu-list-item/menu-list-item.component';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { FeaturesComponent } from './features/features.component';
import { MatDialogModule } from '@angular/material/dialog';
import { MatButtonModule } from '@angular/material/button';
import { ConfirmationDialogComponent } from './ui/confirmation-dialog/confirmation-dialog.component';

@NgModule({
    declarations: [
        AppComponent,
        MenuListItemComponent,
        FeaturesComponent,
        ConfirmationDialogComponent
    ],
    imports: [
        BrowserModule,
        FlexLayoutModule,
        HttpClientModule,
        AppRoutingModule,
        BrowserAnimationsModule,
        MatToolbarModule,
        MatSidenavModule,
        MatListModule,
        MatIconModule,
        MatDialogModule,
        MatButtonModule
    ],
    providers: [],
    bootstrap: [AppComponent]
})
export class AppModule { }

Looking at the Menu

Next, head over to src/app/features/ui/model and edit menu.ts.

import { NavItem } from './nav-item';

export let menu: NavItem[] = [
  {
    displayName: 'Dashboard',
    iconName: 'dashboard',
    route: 'dashboard'
  },
  {
    displayName: 'User',
    iconName: 'face',
    route: 'user',
    children: [
      {
        displayName: 'Account Info',
        iconName: 'account_box',
        route: 'user/account-info'
      }
    ]
  },
  {
      displayName: 'Sign Out',
      iconName: 'highlight_off'
  }
];

The only difference between what you see above and the same file in the previous guide is that last entry in the array. It's a Sign Out link.

You'll note, though, that it has no route. That's because it's not going to route the user to a new URL. Instead, it's going to handle the logout process.

Save that file and edit menu-list-item.component.ts in src/app/features/ui/menu-list-item. Make the following changes:

    constructor(public navService: NavService, public router: Router,
        private authenticationService: AuthenticationService, private dialog: MatDialog) {

        if (this.depth === undefined) {
            this.depth = 0;
        }
    }

...

    onItemSelected(item: NavItem) {
        this.dialog.closeAll();

        if (!item.children || !item.children.length) {
            if (item.route) {
                this.router.navigate([item.route]);
            } else {
                this.handleSpecial(item);
            }
        } 

        if (item.children && item.children.length) {
            this.expanded = !this.expanded;
        }
    }

    handleSpecial(item: NavItem) {
        if (item.displayName == 'Sign Out') {
            this.handleSignOut();
        }
    }

    handleSignOut() {
        const dialogData = new ConfirmationDialogModel('Confirm', 'Are you sure you want to logout?');
        const dialogRef = this.dialog.open(ConfirmationDialogComponent, {
            maxWidth: '400px',
            closeOnNavigation: true,
            data: dialogData
        })

        dialogRef.afterClosed().subscribe(dialogResult => {
            if (dialogResult) {
                this.authenticationService.logout();
            }
        });
    }

The contructor now injects MatDialog so it can create a dialog for the user. 

The onItemSelected() method handles a special case when there's no route associated with the menu item the user clicked. In that situation, the code delegates the logic to the handleSpecial() method.

That handleSpecial() method, in turn, examines the display name of the route and forwards the request to the appropriate method. In this case, it's handleSignOut().

The handleSignOut() method instantiates a ConfirmationDialogModel object. Then, it opens an Angular Material dialog with the ConfirmationDialogComponent that you saw in a previous step. 

That second parameter of the open() method does a lot of work:

  • It maxes out the width of the dialog to 400px
  • It tells Angular to close the dialog if the user ignores it and visits another URL within the app
  • It injects the ConfirmationDialogModel object to into the component

You can see all the configuration options for that second parameter by checking out the docs on MatDialogConfig.

Then, the code subscribes to the Observable returned by the afterClosed() method. You won't be shocked to learn that method is called when the user closes the dialog (either by selecting a button or by closing it).

When the user does close the dialog, the code examines a boolean (dialogResult) that indicates whether or not the user pressed the Yes button. If so, that means the user wants to logout.

And in that case, the code calls the logout() method on AuthenticationService.

Testing Time

Now it's time to take this thing for a test drive. Fire up the UserApplication service you created several lessons ago. Then, go to your command line at the root of your Angular application source and launch it as follows:

ng serve

When everything is loaded, head over to this URL:

http://localhost:4200/login

You should see the login page that looks familiar to you at this point if you've been following along all this time.

Now login with the usual credentials: darth/thedarkside.

Once you're logged in, you should see a dashboard with a different menu:

 

Cool. That Sign Out button is on the menu now.

Go ahead and click it and you should see this:

 

There you go. There's your confirmation dialog.

Now click No and the dialog box should simply disappear.

Once again, click the Sign Out menu item on the left-hand sidebar. This time, click Yes when the dialog appears.

It should log you and take you back to the login page.

Wrapping It Up

Congratulations! You now have a working dialog box for your CRM app.

Even better, you created the dialog box so it's reusable by other components and even other modules.

Now tinker with it a bit. Change the look and feel. Add your own messaging. Make it bigger or smaller.

And remember, you can always grab the complete source from GitHub.

Have fun!