Delete confirmation dialog using Angular and reactive programming with ngrx.
Demo
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’schangeDetection
toOnPush
. 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 theMD_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 theMdDialogRef
class, which provides a reference to the dialog that was opened via theMdDialog
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 combinedState
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 customdata
property, themdDialogRef
for reference to our current dialog, and thestore
. - The
cancel()
method willdispatch()
an action when the user clicks/taps the cancel button. The action is optional, so we check if it is defined first. We thenclose()
the dialog. - The
close()
method closes the dialog that is displayed. - The
delete()
method willdispatch()
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 theonEsc()
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 optionalAction
todispatch()
when the user presses the cancel button. - The
delete
property is a requiredAction
todispatch()
when the user presses the delete button. - The
text
property is a requiredstring
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 requiredstring
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 theDeleteConfirmDialogComponent
component. - Next, we define a new property in our class that is decorated with the
@Effect()
decorator. This effect will only be executed for theREMOVE_HERO_CONFIRM_DIALOG_OPEN
action. - Using the
MdDialog
service (that was previously injected into our class in theconstructor()
function) weopen()
theDeleteConfirmDialogComponent
component. - Note that we also specify the custom
data
property for the dialog, specifying thepayload
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!