« Back to home

Angular 2 Confirm Dialog Component

Daj się poznać

This post is a continuation of the previous one. I must honestly say that while writing this post I felt a bit guilty because I wrote about something which I created in my project but I don’t need yet. I believe in You Aren’t Gonna Need It but while writing this post I violated my belief. I described in this post how I created an Angular 2 confirm dialog component by wrapping a Material Design Lite (MDL) dialog component. I feel awkward because I don’t need this component yet. I didn’t have a use case for this component. I implemented it upfront which was a violation of the YAGNI rule. Yes, I am aware of this. To justify my actions, I have only one reason: it would be nice to implement this component and write about it now because it corresponds with my previous post and it’s quite likely that I will need this component in the future so my work won’t be wasted.

The first thing I did was to create the following structure of folders and files.

Folders tree

Then I added the following content to my confirm.component.html file:

<div id="confirmationModal" class="dialog-container">
   <div class="mdl-card mdl-shadow--16dp">
       <h5>{{title}}</h5>
       <p>{{message}}</p>
       <div class="mdl-card__actions dialog-button-bar">
           <button class="mdl-button mdl-js-button mdl-button--raised mdl-js-ripple-effect" id="cancelButton"
                   data-upgraded=",MaterialButton,MaterialRipple">{{cancelText}}<span
                   class="mdl-button__ripple-container"><span class="mdl-ripple"></span></span>
           </button>
           <button class="mdl-button mdl-button--accent mdl-button--raised mdl-js-button mdl-js-ripple-effect"
                   id="okButton" data-upgraded=",MaterialButton,MaterialRipple">{{okText}}<span
                   class="mdl-button__ripple-container"><span class="mdl-ripple"></span></span>
           </button>
       </div>
   </div>
</div>

Nothing special here. You can find out more about MDL dialogs here.

I’ll be using this dialog through the service which is defined in the file confirm.service.ts and the whole file looked like this:

import {Injectable} from "angular2/core";

@Injectable()
export class ConfirmService {
   activate: (message?: string, title?: string) => Promise<boolean>;
}

As you can see there is only one property in the class ConfirmService which has type function. This function takes two optional string parameters: message and title and returns Promise of type boolean which will indicate of course whether a user confirms something or not. I’ll assign value to this property in the ConfirmComponent constructor which you will see in a minute.

If you wonder what @Injectable() means, it’s a decorator. This decorator is required if you have any dependencies in your service. In my case, this isn’t true but it is recommended to always decorate services with this decorator. You can read about it here.

The key part is in the confirm.component.ts file. I am not going to show this whole file here, you can see it at this link. But I wrote about the crucial parts of this component. One of them is constructor:

constructor(confirmService:ConfirmService) {
   confirmService.activate = this.activate.bind(this);
}

In the constructor, I assigned a function to the property activate of ConfirmService. After this, calling activate on ConfirmService will cause the function activate from ConfirmComponent to be executed.

Another important part of this component is method ngOnInit from interface OnInit.

ngOnInit():any {
   this._confirmElement = document.getElementById('confirmationModal');
   this._cancelButton = document.getElementById('cancelButton');
   this._okButton = document.getElementById('okButton');
}

This method collects references to the elements from view and assigns them to private fields in my component class. This why I got access to the dialog itself, the cancel button and the ok button.

Having these references allows me to assign handlers to on click events on buttons and I can show a modal dialog. These happened in _show method:

On ok click:

this._okButton.onclick = ((e:any) => {
   e.preventDefault();
   if (!positiveOnClick(e)) this._hideDialog()
});

On cancel click:

this._confirmElement.onclick = () => {
   this._hideDialog();
   return negativeOnClick(null);
};

And to Show dialog:

this._confirmElement.style.opacity = 1;

And of course I used modal dialog element reference to hide the dialog in method _hideDialog

private _hideDialog() {
   document.onkeyup = null;
   this._confirmElement.style.opacity = 0;
   window.setTimeout(() => this._confirmElement.style.zIndex = -1, 400);
}

The last import method in ConfirmComponent is activate, and this one is assigned to activate property of ConfirmService. The code of this method is this:

activate(message = this._defaults.message, title = this._defaults.title) {
   this._setLabels(message, title);

   let promise = new Promise<boolean>(resolve => {
       this._show(resolve);
   });
   return promise;
}

The responsibilities of this method are to set the title and message properties, create the promise, and passed it to _show method and finally return this promise to the caller. Nothing fancy here.

The confirm is needed throughout the whole application. That’s why I added it to the AppComponent. Angular 2 has something which is called hierarchical dependency injection system. In short, it means that if a parent component has something injected as a dependency, in my case AppComponent, their children will get the same instance of this when they request it. As all my components will be children of AppComponent, all of them will get access to the same ConfirmSerivice instance. My AppComponent at that moment looked like this:

import {Component, OnInit} from 'angular2/core';

import {ConfirmService} from "./shared/confirm/confirm.service";
import {ConfirmComponent} from "./shared/confirm/confirm.component";

declare var componentHandler:any;

@Component({
   selector: 'notifier',
   templateUrl: 'app/app.component.html',
   directives: [ConfirmComponent],
   providers: [
       ConfirmService
   ]
})
export class AppComponent implements OnInit {

   title = "Notifier!!!";

   constructor(private _confirmService:ConfirmService) {
   }

   showConfirmDialog() {
       this._confirmService.activate("Are you sure?")
           .then(res => console.log(`Confirmed: ${res}`));
   }

   ngOnInit():any {
       componentHandler.upgradeDom();
   }
}

Some parts of this file are especially important, for example, this line:

  directives: [ConfirmComponent],

I said that my component will be using other components, for now only one is specified: ConfirmComponent.

In this fragment of code:

 providers: [ConfirmService]

I registered providers required by the AppComponent. Actually, I only specified the type - ConfirmService, and Angular 2 knows what it should do. I could be more verbose and instead of this shorthand syntax, I could write:

providers: [
    provide(ConfirmService, {useClass: ConfirmService})
]

But there is no reason to do this as you can use something more concise.

Another important part in AppComponent is its constructor which looked like this:

constructor(private _confirmService:ConfirmService) {
}

I used shorthand syntax which created a private field for me and at the same time a parameter of constructor. This field is of type ConfirmService and an instance of that type is provided by Angular 2 dependency injection system.

The method showConfirmDialog had nothing special:

showConfirmDialog() {
   this._confirmService.activate("Are you sure?")
       .then(res => console.log(`Confirmed: ${res}`));
}

I opened the confirm dialog and when it is closed I printed the choice that was made in the console.

A very important thing happened in the method ngOnInit.

ngOnInit():any {
   componentHandler.upgradeDom();
}

This line is especially important:

 componentHandler.upgradeDom();

Material Design Lite will automatically register and render all elements marked with MDL classes upon the page load. However, in the case where you are creating DOM elements dynamically, you need to register new elements by yourself using either componentHandler.upgradeDom or componentHandler.upgradeElement. Without this line, MDL components won’t work with Angular 2.

And that’s it. That’s my whole work. I added only to app.component.html:

<modal-confirm></modal-confirm>

I also added some styling to the ConfirmComponent by adding this line in the component decorator:

  @Component({
  //...
   styleUrls: ['app/shared/confirm/confirm.component.css']
})

And my final result looked like this:

Final result

After clicking Ok I got this in a web browser console:

Final result in console

You can see the whole source code of this solution in the github repository for this project.

My implementation of the confirm dialog component is based on the work of John Papa from the Angular 2 First Look Pluralsight course. I made some changes to adjust his solution to my reality. In the next post, I am going to write about messing around with the application setup but this time from a different perspective. See you then.

Related posts:

Comments

comments powered by Disqus