Picture of Brian Love wearing black against a dark wall in Portland, OR.

Brian Love

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:

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:

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:

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!