How to Create a Simple Dialog Box with Angular Material

Image by geralt on Pixabay

In a web application, dialog boxes are convenient tools to interact with users. It can attract users’ attention if used properly and will make your webpage look neat. It needs some advanced CSS knowledge if you want to build a dialog box from scratch. However, using a framework like Angular, creating dialog boxes is a very simple task, even for someone who is not an expert in CSS. In this post, we will demonstrate how to create a simple dialog box with Angular. All the details as well as some caveats are covered which can be helpful for your work in practice.


Preparation

You need to install node.js first because we need to use the npm tool to install Angular CLI.

$ npm install -g @angular/cli

Alternatively, you create the project in StackBlitz if you don’t want to do it locally.


Create the workspace and components needed

An angular application is developed in the context of an Angular workspace. We need to create one first:

$ ng new mydialog

A workspace is basically a project folder containing all kinds of boilerplate code. For now, you don’t need to check the code yet. If you use StackBlitz, a workspace will be created for you by default when you choose the Angular framework.

A default NgModule (AppModule) will be created for us, which includes the modules, components, services, etc for the application. Besides, a default component (AppComponent) is created as well which serves as the entry point for the application.

We need to create a new component that will handle the logic for the dialog box:

$ ng generate component add-cart

A new folder will be created inside the app folder for the new component (AddCartComponent) which includes the files for this component.

Don’t get scared by the large number of files shown here, we normally focus only on the files in the red box shown above.


Install Angular Material

We will focus on functionality and data logic, rather than styling in this post. Nonetheless, we will use Angular Material to give our buttons and input forms some nice styling that is easy to apply. And most importantly, we will use the MatDialog service to open modal dialogs with Material Design styling and animations.

To install Angular Material in your project, run the following command with Angular CLI:

$ ng add @angular/material

We can keep the default choices during the installation process. Your project files such as package.json, index.html, and styles.css will be automatically updated to apply the Angular Material styling.

In StackBlitz, we need to add a dependency for Angular Material:

Following the instruction shown in the picture above to add the dependency for Angular Material. Besides, we need to import a theme in styles.css to make Angular Material work properly in StackBlitz.

/* Need to import a theme to make it work properly in StackBlitz */
@import '@angular/material/prebuilt-themes/deeppurple-amber.css';

Import the Angular Material modules

In the previous step, Angular CLI only set up the global configurations of Angular Material. We need to manually import the modules that will be used in our application in app.module.ts:

/* src/app/app.module.ts */

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

// Needed for building template driven forms with `ngModel`.
import { FormsModule } from '@angular/forms';

// Imported by Angular CLI automatically for Angular Material.
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';

// You need to import the material modules manually that are needed in the
// application.
import { MatButtonModule } from '@angular/material/button';
import { MatDialogModule } from '@angular/material/dialog';
import { MatInputModule } from '@angular/material/input';
import { MatFormFieldModule } from '@angular/material/form-field';

// Added automatically when the component was created with Angular CLI,
// but you need to import them manually in StackBlitz.
import { AppComponent } from './app.component';
import { AddCartComponent } from './add-cart/add-cart.component';

@NgModule({
  // Components are put in `declarations`.
  declarations: [AppComponent, AddCartComponent],
  // Modules are put in `imports`.
  imports: [
    BrowserModule,
    BrowserAnimationsModule,
    FormsModule,
    MatButtonModule,
    MatDialogModule,
    MatFormFieldModule,
    MatInputModule,
  ],
  providers: [],
  bootstrap: [AppComponent],
})
export class AppModule {}

Create a button to trigger the dialog

First, we need to create something that can work as the trigger to open the dialog box. A button is preferred in this case.

Add the following code in app.component.html to create the button. Besides, we will also show the cart data once the dialog is closed.

<div class="add-cart-section">
  <button
    mat-raised-button
    class="add-cart-button"
    (click)="openAddCartDialog()"
  >
    {{ cartData === undefined ? "Add to cart" : "Edit cart" }}
  </button>

  <div *ngIf="cartData" class="price-section">
    <h3>Your order:</h3>
    <p>Price: {{ cartData.price }}, quantity: {{ cartData.quantity }}</p>
  </div>
</div>

The data logic is included in the model file app.compnent.ts:

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

import { CartData, AddCartComponent } from './add-cart/add-cart.component';

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

  // Initial price and quantity are obtained from somewhere:
  price = 199;
  quantity = 2;

  // `dialog` is injected as an instance of the `MatDialog` service.
  // It can be used to open a dialog popup.
  constructor(public dialog: MatDialog) {}

  openAddCartDialog() {
    // We either create a new cart item or pass the existing one so it can
    // be edited.
    let dataToPass: CartData;

    if (this.cartData) {
      dataToPass = this.cartData;
    } else {
      dataToPass = {
        price: this.price,
        quantity: this.quantity,
      };
    }

    // The target component for the dialog is passed as the first
    // argument. The second argument is the config object. The most
    // common configuration is `width` which sets the width of the dialog
    // popup, and the `data` which passes data to the dialog component.
    const dialogRef = this.dialog.open(AddCartComponent, {
      width: '300px',
      data: { ...dataToPass },
    });

    // Some data can be passed back when the dialog is closed, using
    // the `mat-dialog-close` attribute, or the `close()` method of the
    // the dialog reference.
    dialogRef.afterClosed().subscribe((result) => {
      this.cartData = result === undefined ? this.cartData : result;
    });
  }
}

The code should be fairly self-explanatory, especially with the comments. It may look simple, but if you want to have it work properly with no bugs, you would need to be very careful about all the details. This is my first frontend post, which I found is much more difficult to write than data or backend ones 😅. Nonetheless, this post includes some tricks that will be helpful for your work, especially if you are also switching a bit from backend to frontend. It can supplement the official documentation with some hands-on experience learned from practical projects.

If you want to have the exact styling as shown in this example, you would need to write some CSS code as can be found in this repo.


Update the dialog model and temple files

Let’s create the model file first, which is simpler than the template file. Add the following code to add-cart.component.ts:

/* src/app/add-cart/add-cart.component.ts */

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

// A simple interface that works as a type for the cart data.
export interface CartData {
  price: number;
  quantity: number;
}

@Component({
  selector: 'app-add-cart',
  templateUrl: './add-cart.component.html',
  styleUrls: ['./add-cart.component.css'],
})
export class AddCartComponent {
  /* `dialogRef` is ingested by Angular and works as a handler for the
   * dialog opened.
   * `MAT_DIALOG_DATA` injects data that is passed when the dialog is
   * opened.
   */
  constructor(
    public dialogRef: MatDialogRef<AddCartComponent>,
    @Inject(MAT_DIALOG_DATA) public cartData: CartData
  ) {}

  onCancel() {
    // We can use the dialog handler to close the dialog explicitly.
    // It can also be closed with the `mat-dialog-close` attribute.
    this.dialogRef.close();
  }
}

Something that’s worth mentioning here is that we can use the @Inject(MAT_DIALOG_DATA) syntax to inject some data into the dialog component. The data is passed from the component where the dialog is opened, as shown above, with the data configuration.

And the template file is a bit more complex, which requires some basic knowledge of Angular and Angular Material, especially mat-form-field and MatDialog, as well as template-driven forms.

<h1 mat-dialog-title class="cart-title">Your cart info</h1>
<div mat-dialog-content>
  <p>Please enter the price and quantity:</p>
  <mat-form-field appearance="outline" class="fix-prefix" floatLabel="always">
    <mat-label>Price</mat-label>
    <span matPrefix>$&nbsp;</span>
    <input
      matInput
      type="number"
      placeholder="0.00"
      autocomplete="off"
      [(ngModel)]="cartData.price"
    />
  </mat-form-field>
  <mat-form-field appearance="outline" class="fix-prefix" floatLabel="always">
    <mat-label>Quantity</mat-label>
    <span matPrefix>#&nbsp;</span>
    <input
      matInput
      type="number"
      placeholder="1"
      autocomplete="off"
      [(ngModel)]="cartData.quantity"
    />
  </mat-form-field>
</div>

<div mat-dialog-actions class="buttons">
  <button mat-raised-button class="button cancel-button" (click)="onCancel()">
    Cancel
  </button>
  <button
    mat-raised-button
    [mat-dialog-close]="cartData"
    class="button save-button"
  >
    {{ cartData === undefined ? "Create" : "Save" }}
  </button>
</div>

Again, if you want to have the exact styling as shown in this example, you would need to write some CSS code as can be found in this repo.


Spin up the application and fix an annoying bug

Actually, we can and should spin up the application at the very beginning of the development stage so you see the result and fix bugs in real-time.

We can spin up the application with the following command:

$ ng serve --open

The browser will open automatically for you. When you click the “Add to cart” button you will see a dialog box as follows:

As we see, the input box and prefix are not aligned properly. This is a known bug and should be fixed explicitly. We need to add the following CSS code to the global stylesheet styles.css:

/* You can add global styles to this file, and also import other style files. */

/* Need to import a theme to make it work properly in StackBlitz. */
@import '@angular/material/prebuilt-themes/deeppurple-amber.css';

html, body { height: 100%; }
body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; }

/* Need this to fix the prefix that is improperly aligned in mat-form-field. */
.fix-prefix.mat-form-field-appearance-outline .mat-form-field-prefix {
    top: 0;
}

And then we need to add the fix-prefix CSS class to the mat-form-field with a prefix for the input box:

<mat-form-field appearance="outline" class="fix-prefix" floatLabel="always">
    <mat-label>Price</mat-label>
    <span matPrefix>$&nbsp;</span>
    <input
      matInput
      type="number"
      placeholder="0.00"
      autocomplete="off"
      [(ngModel)]="cartData.price"
    />
  </mat-form-field>
  <mat-form-field appearance="outline" class="fix-prefix" floatLabel="always">
    <mat-label>Quantity</mat-label>
    <span matPrefix>#&nbsp;</span>
    <input
      matInput
      type="number"
      placeholder="1"
      autocomplete="off"
      [(ngModel)]="cartData.quantity"
    />
  </mat-form-field>

Now the application should look nice and correct, cheers!


It’s highly recommended that you download the repo for this post locally or play with the existing one in StackBlitz so that you can have a better understanding of the code. Try to change the code, break it and then fix it. You will learn much more when you begin to code along than merely reading documentation or videos.



Leave a comment

Blog at WordPress.com.