Brian F Love
Learn from a Google Developer Expert focused on Angular, Web Technologies, and Node.js from Portland, OR.
Ad·ultimatecourses.com
Learn Angular the right way with Ultimate Courses

Angular Delete Confirmation

Delete confirmation dialog using Angular and reactive programming with ngrx.

Demo

Demo of delete confirmation dialog

Source Code

You can download the source code and follow along or fork the repository on GitHub:

Install

To get started, clone my sample mean-material-reactive application:

$ git clone https://github.com/blove/mean-material-reactive.git

Then, run the gulp tasks and start the Node.js Express server. This will create a simple REST API endpoint at http://localhost:8080/heros.

$ gulp
$ chmod +x ./dist/bin/www
$ ./dist/bin/www

Then, serve the Angular client using the CLI:

$ ng serve

Confirmation Dialog

Right now, if you add a hero to the application and then click the delete icon, the hero is immediately removed. It would be nice if our users had to confirm the deletion, just in case they accidentally clicked the delete icon.

Let's use the Angular CLI to generate a new DeleteConfirmDialogComponent component:

$ ng g c shared/delete-confirm-dialog

Now, open the client/src/app/shared/delete-confirm-dialog/delete-confirm-dialog.html template. The template will contain a title and text that is specified from the calling component. We will also have two buttons; one to cancel and the other to confirm the deletion:

<div class="header">
  <button md-dialog-close md-icon-button>
    <md-icon>close</md-icon>
  </button>
  <h1 md-dialog-title>{{ data.title }}</h1>
</div>
<md-dialog-content>
  <p [innerHTML]="data.text"></p>
</md-dialog-content>
<md-dialog-actions>
  <button md-button (click)="cancel()">Cancel</button>
  <button md-button (click)="delete()" color="primary">Delete</button>
</md-dialog-actions>

Now, let's take a look at the DeleteConfirmDialogComponent at client/src/app/shared/delete-confirm-dialog.component.ts:

import {
  ChangeDetectionStrategy,
  Component,
  HostListener,
  Inject
} from '@angular/core';
import { MD_DIALOG_DATA, MdDialogRef } from "@angular/material";
import { Action, Store } from "@ngrx/store";
import { State } from "../../app.reducers";

@Component({
  changeDetection: ChangeDetectionStrategy.OnPush,
  selector: 'app-delete-confirm-dialog',
  templateUrl: './delete-confirm-dialog.component.html',
  styleUrls: ['./delete-confirm-dialog.component.scss']
})
export class DeleteConfirmDialogComponent {

  constructor(
    @Inject(MD_DIALOG_DATA) public data: {
      cancel?: Action,
      delete: Action,
      go?: Action,
      text: string,
      title: string
    },
    private mdDialogRef: MdDialogRef<DeleteConfirmDialogComponent>,
    private store: Store<State>
  ) { }

  public cancel() {
    if (this.data.cancel !== undefined){
      this.store.dispatch(this.data.cancel);
    }
    this.close();
  }

  public close() {
    this.mdDialogRef.close();
  }

  public delete() {
    this.store.dispatch(this.data.delete);
    if (this.data.go !== undefined) {
      this.store.dispatch(this.data.go);
    }
    this.close();
  }

  @HostListener("keydown.esc")
  public onEsc() {
    this.close();
  }

}

Let's review our new component:

  • First, we import the ChangeDetectionStrategy enum and set our component's changeDetection to OnPush. This will inform the Angular change detection that we only need to run change detections a single time, when the component is hydrated. This is good practice for our stateless components.
  • We import the HostListener class so that we can listen for events on our host component, including the keypress of the escape (esc) key. We will close the dialog if the user presses the escape key.
  • We import the Inject interface so that we can use the MD_DIALOG_DATA injection token to get access to the custom data provided to the dialog when it is constructed.
  • Next, we import the MD_DIALOG_DATA injection token and the MdDialogRef class, which provides a reference to the dialog that was opened via the MdDialog service, which is our current dialog component.
  • We import the Action interface from the @ngrx/store module because our custom data that is passed to our dialog will contain several actions, including the action to take when the user confirms the deletion.
  • We import the Store and our application's combined State so that we can dispatch actions to our store based on what is provided to our dialog.
  • In the constructor() function we inject our dialog's custom data property, the mdDialogRef for reference to our current dialog, and the store.
  • The cancel() method will dispatch() an action when the user clicks/taps the cancel button. The action is optional, so we check if it is defined first. We then close() the dialog.
  • The close() method closes the dialog that is displayed.
  • The delete() method will dispatch() the action specified when the user clicks/taps on the delete button to confirm the deletion, and then closes the dialog.
  • Finally, we use the HostListener decorator to invoke the onEsc() method when the user presses the escape (esc) key, which closes the dialog.

While the Angular CLI will update our SharedModule module to import the newly generated component, we will need to include the class definition in the module's entryComponents array in the decorator:

@NgModule({
  entryComponents: [HeroCreateDialogComponent, DeleteConfirmDialogComponent],

  // code omitted
})
export class SharedModule {}

Let's also add some style to our component to display the close button in the top-right of the dialog, as well as adding some margin between our two buttons:

:host {
  .header {
    position: relative;

    button[md-dialog-close] {
      position: absolute;
      top: -16px;
      right: -16px;
    }
  }

  md-dialog-actions {
    button {
      margin-left: 16px;

      &:first-child {
        margin-left: 0;
      }
    }
  }
}

Action

The next step is to define an action to open the deletion confirmation window. We'll define the action in the client/src/app/heros/heros.actions.ts file:

export const REMOVE_HERO_CONFIRM_DIALOG_OPEN = '[heros] Remove hero confirm dialog open';

export class RemoveHeroConfirmDialogOpen implements Action {
  readonly type = REMOVE_HERO_CONFIRM_DIALOG_OPEN;
  constructor(public payload: {
    cancel?: Action,
    delete: Action,
    text: string,
    title: string
  }) {}
}

export type Actions =
  CreateHeroAction
  | CreateHeroErrorAction
  | CreateHeroSuccessAction
  | CreateHeroDialogCloseAction
  | CreateHeroDialogOpenAction
  | LoadHerosAction
  | LoadHerosErrorAction
  | LoadHerosSuccessAction
  | RemoveHeroAction
  | RemoveHeroErrorAction
  | RemoveHeroSuccessAction
  | RemoveHeroConfirmDialogOpen;

Note, I have omitted the other actions and code in the file. Rather, I am just showing the additional constant string action, the RemoveHeroConfirmDialogOpen class that implements the Action interface, and adding the class to our Actions type declaration.

The RemoveHeroConfirmDialogOpen class's constructor() function requires a payload object with several properties:

  • The cancel property is an optional Action to dispatch() when the user presses the cancel button.
  • The delete property is a required Action to dispatch() when the user presses the delete button.
  • The text property is a required string that is displayed to the user in the contents of the dialog. This is most likely some text to confirm with the user that they are about to delete something.
  • The title property is a required string that is displayed in the header of the dialog.

Side Effect

The next step is to create a new side effect that will open the dialog when the REMOVE_HERO_CONFIRM_DIALOG_OPEN action is dispatched. Let's add this to the HeroEffects class in client/src/app/heros/heros.effects.ts:

import {
  REMOVE_HERO_CONFIRM_DIALOG_OPEN
} from "./heros.actions";

import { DeleteConfirmDialogComponent } from "../shared/delete-confirm-dialog/delete-confirm-dialog.component";

@Injectable()
export class HeroEffects {

  // code omitted

  @Effect()
  public removeHeroConfirmDialogOpen: Observable<Action> = this.actions
    .ofType(REMOVE_HERO_CONFIRM_DIALOG_OPEN)
    .map(toPayload)
    .switchMap(payload => {
      this.mdDialog.open(DeleteConfirmDialogComponent, {
        data: payload
      });
      return empty();
    });

}

Note that I have omitted some code in the excerpt above, both in the HeroEffects class as well as in the imports statements.

Here is what is important to note:

  • First, we import the REMOVE_HERO_CONFIRM_DIALOG_OPEN constant string value for our action as well as the DeleteConfirmDialogComponent component.
  • Next, we define a new property in our class that is decorated with the @Effect() decorator. This effect will only be executed for the REMOVE_HERO_CONFIRM_DIALOG_OPEN action.
  • Using the MdDialog service (that was previously injected into our class in the constructor() function) we open() the DeleteConfirmDialogComponent component.
  • Note that we also specify the custom data property for the dialog, specifying the payload object that was dispatched with the action.

Dispatch Action

The current state of our application is such that when the user presses the remove button the hero is immediately removed. So, our final step is to swap out the current action that removes the hero with our delete confirmation dialog by invoking the newly defined RemoveHeroConfirmDialogOpen action.

Let's update the IndexComponent in client/src/app/heros/index/index.component.ts:

import { Component, OnInit } from '@angular/core';
import { Observable } from "rxjs/Observable";
import { Store } from "@ngrx/store";
import { State, getHeros } from "../../app.reducers";
import { Hero } from "../../models/hero";
import { LoadHerosAction, RemoveHeroAction, RemoveHeroConfirmDialogOpen } from "../heros.actions";

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

  public heros: Observable<Array<Hero>>;

  constructor(private store: Store<State>) { }

  ngOnInit() {
    this.heros = this.store.select(getHeros);
    this.store.dispatch(new LoadHerosAction());
  }

  public remove(hero: Hero) {
    this.store.dispatch(new RemoveHeroConfirmDialogOpen({
      delete: new RemoveHeroAction({ hero: hero }),
      text: `Are you sure you want to remove the hero <em>${hero.name}</em> from the tour of heros?`,
      title: "Remove Hero"
    }));
  }

}

Most of this component was created in my previous post on building an Angular app using reactive programming.

First, I am importing our new RemoveHeroConfirmDialogOpen action. Next, I updated the remove() method to dispatch this new action. I am specifying the required properties for the payload object, specifically the delete, text and title properties. Note that the delete property is an instance of the RemoveHeroAction, which was previously being dispatched directly in this method. Now, the action will be displatched in the DeleteConfirmDialogComponent when the user clicks/taps on the delete button.

Reusable

One of the goals of this approach is to make a reusable component. We can use the DeleteConfirmDialogComponent whenever we want to present the user with a confirmation dialog before deleting something. There is no need to create multiple components, rather, we can just create new actions and effects for opening the dialog based on our application's needs. Very cool!

Brian F Love

Hi, I'm Brian. I am interested in TypeScript, Angular and Node.js. I'm married to my best friend Bonnie, I live in Portland and I ski (a lot).