New version Angular 9
AngularJS Tutorial - 6.1
In this part we will discover how to build a dialog service.

Download
The source for this part is here: 06_dialog.zip.
The library created based on this tutorial is on the sgDialogService
The samples can be run with Firefox, in general or by running, into the directory,
-
If you have Python 2.x: python -m SimpleHTTPServer 8000
-
If you have Python 3.x: python -m http.server 8000
-
If you have Perl:
- cpan HTTP::Server::Brick # install dependency
- perl -MHTTP::Server::Brick -e '$s=HTTP::Server::Brick->new(port=>8000); $s->mount("/"=>{path=>"."}); $s->start'
-
If you are under windows: "C:\Program Files (x86)\IIS Express\iisexpress.exe" /path:%CD% /port:8000
NOTE: To use the dialog service is only needed to look into /app/commn, copy the style.css and dialog.html,
taking the javascript identified by "MODAL DIALOG SERVICE BEGIN"!!
The dialog - Without Angular-UI
Define the global template
We will follow the standard dialog template from Bootstrap. This will be saved as "app/common/dialog.html"
The items surrounded with sharps will be replaced by the service when creating the real dialog.
Note the modalButtons inside the modal footer. Those must be created into the controller.
<div class='modal-dialog' id='modalDialog' name='modalDialog'>
<div class='modal-content '>
<div class='modal-header'>
<button type='button' class='close' ng-click='#closeModal#' aria-label='Close'><span aria-hidden='true'>×</span></button>
<h4 class='modal-title' >#modalTitle#</h4>
</div>
<div class='modal-body'>
#modalBody#
</div>
<div class='modal-footer' >
<button ng-repeat='button in modalButtons' type='button'
class='btn {{button.class}}'
ng-click='button.action()' ng-disabled="button.disabled()">{{button.text}}</button>
</div>
</div>
</div>
CSS
This is the css that will handle the dialog (mixed with the bootstrap css).
Note the media query. This will allow the dialog to fill the viewport if, for example,
the page is viewed on a mobile.
The modalHidden class is used to show or hide the whole dialog and overlay.
@media (max-width: 600px) {
.modal-dialog {
top: 0;
right: 0;
bottom: 0;
left: 0;
}
.modal-content {
top: 0;
right: 0;
bottom: 0;
left: 0;
}
}
.modalDialogOverlay {
position: fixed;
font-family: Arial, Helvetica, sans-serif;
top: 0;
right: 0;
bottom: 0;
left: 0;
background: rgba(0,0,0,0.2);
z-index: 1000;
opacity:.8;
-webkit-transition: opacity 400ms ease-in;
-moz-transition: opacity 400ms ease-in;
transition: opacity 400ms ease-in;
pointer-events: auto;
}
.modalHidden{
display:none;
pointer-events: none;
}
The dialog service
We will create for this a service, called "globalModalService". The dependencies will be:
- $http: The http service to call apis and get files
- $templateCache: The cache in which the Angular templates are stored
- $controller: The controller factory
- $compile: The service to transform html strings in angular items
- $rootScope: The main scope of the application
- $document: Angular wrapper for the DOM document
common.service('globalModalService',['$http', '$templateCache', '$controller', '$compile','$rootScope','$document',
function($http, $templateCache, $controller, $compile,$rootScope,$document) {
...
Then the variables needed. Modal instance will contain the tipical parts of the dialog, used by the dialog controller.
//Initial position of the floating dialog
var startX, startY;
//Position where the mouse clicked on the dialog
var initialMouseX, initialMouseY;
//Content of the main template body
var dialogTemplateBody = "";
//Instance
var modalInstance = {};
...
Load the template content into the service local variable. For this resource will be used the templateCache as...cache
$http.get('app/common/dialog.html', { cache: $templateCache })
.then(function (response) {
dialogTemplateBody = response.data;
});
...
The overlay (the grey shade) is created to allow modal operations.
First is checked if the overlay is already present
The content of the overlay will be compiled into an angular item, and initialized using the rort scope as scope.
Then the item is appended to the document body
...
var overlay = document.getElementById("modalDialogOverlay");
if(!overlay){
var overlayContent = "<div id='modalDialogOverlay' name='modalDialogOverlay' class='modalDialogOverlay modalHidden'></div>";
//Setup the overlay and append to context
var compiledOverlay = $compile(overlayContent)($rootScope);
document.body.appendChild(compiledOverlay[0]);
}
Then a function is created to open the modal. We setup the modal instance with the overlay. Will be used later.
The setup parameter is an object containing:
- controller (optional): The name of the controller that will be instantiated for the template.
It is optional if the template contains an ng-controller directive - template (mandatory): The path of the dialog content template.
- title (mandatory): The title of the dialog
- closeModal (optional): Default to "closeModal". The function that will be exposed on the controller
scope to close the modal dialog - modalButtons (optional): Default to "modalButtons". The variable on the controller scope that will
contain the list of buttons to show on the dialog. - data (optional): The object containing the variable to add to the controller scope.
- callback (optional): An empty parameter function called whenever the dialog is closed.
this.openModal = function(setup){
modalInstance = {
overlay : angular.element(document.getElementById("modalDialogOverlay"))
};
...
The template is loaded and its content is stored into a variable
$http.get(setup.template, { cache: $templateCache })
.then(function (response) {
var contentTemplate = response.data;
...
We search for the occurrences of the ng-controller string inside the template.
This is needed since we want to control the creation of the scope. If we leave the ng-controller
Angular will create a new, not accessible, scope. This is added to find the name of the controller
and instantiate it anyways. After having set the controller on the "setup" object any reference to
the controller is removed from the template.
if(!setup.controller){
//Seek the first ng-controller
var controller = response.data.match(/ng-controller(\s\S)*=(\s\S)*("|')([^"']*)("|')/mi);
if(controller!=null){
//Get the name (the 4th match group)
setup.controller = controller[4];
//And remove it from the content
contentTemplate = contentTemplate.replace(controller[0],"");
}
}
The parameters are initialized with default values if not present. This function will be explained later.
defaultParameters(setup);
A new scope is created for the controller
templateScope = $rootScope.$new();
We then setup the close function on our "modal instance". When called makes the overlay invisible,
execute the callback passed by the setup and destroy the dialog.
modalInstance.closeModal = function(){
//Make the overlay invisibile
modalInstance.overlay.toggleClass('modalHidden');
//Remove the dialog
modalInstance.dialog.remove();
//Execute the optional callback
if(setup.callback){
setup.callback();
}
}
Add the close function to the scope (for the close button!) and copy the data on the template.
This is made leveraging on the fact that javascript objects are exactly like hashmaps.
templateScope[setup.closeModal] = modalInstance.closeModal;
for(var prop in setup.data){
templateScope[prop] = setup.data[prop];
}
Then we initialize the template injecting the dependencies. The first parameter of $controller is the
name of the controller. The second parameter is an object containing the name/value pairs of the items
that will be passed as dependencies to the controller. Between the parameters there is the scope we
just created that will be the scope seen by the controller.
if(setup.controller){
$controller(setup.controller, {
$scope: templateScope,
modalInstance: modalInstance
});
}
Now we will modify the template with the values passed with setup
var realTemplate = dialogTemplateBody.replace("#modalTitle#","{{"+setup.title+"}}");
//Set the iterator on the buttons
realTemplate = realTemplate.replace("button in modalButtons","button in "+setup.modalButtons);
//Insert the function to call to close the modal
realTemplate = realTemplate.replace("#closeModal#",setup.closeModal+"()");
//Insert the template into the dialog context
realTemplate = realTemplate.replace("#modalBody#",contentTemplate);
We then create the content. Compiling the template with all replacements connecting it with the scope.
We then toggle the visibility of the overlay to show it and we save the instance of the dialog
var element = $compile(realTemplate)(templateScope);
//And append at the end of body
document.body.appendChild(element[0]);
//Show the overaly
modalInstance.overlay.toggleClass('modalHidden');
//Get the connected angular element
modalInstance.dialog = angular.element(element);
Now we will place the dialog into the middle of the screen. We will cover this function later.
centerTheDialog(); }); }
Now we will add the initalization function:
var defaultParameters = function(setup){
if(!setup.data){setup.data = {};}
if(!setup.title){setup.title = 'title'; }
if(!setup.modalButtons){setup.modalButtons = 'modalButtons';}
if(!setup.closeModal){setup.closeModal = 'closeModal';}
}
And the dialog centering function. The left position will be obtained getting the total width of
the page and subtracting the width of the dialog. At the point when it's called the rendering
have already complete, so we have the -REAL- size of the dialog,
var centerTheDialog = function(){
var w = Math.max(document.documentElement.clientWidth, window.innerWidth || 0);
pos = w/2-modalInstance.dialog.prop('offsetWidth')/2;
modalInstance.dialog.css({
position: 'absoulte',
left:pos+'px'
});
}
Draggable Dialog
First we handle the click on a draggable are (the dialog header). This will be placed after the call
to "modalInstance.overlay.toggleClass('modalHidden');".
The mousedown is binded on the header, and are binded the mouseup and move on the document.
...
modalInstance.overlay.toggleClass('modalHidden');
var gripElement = angular.element(element[0].getElementsByClassName('modal-header'));
//Bind the mousemove
gripElement.bind('mousedown', function($event) {
//Retrieve the absolute offset of the whole item
startX = modalInstance.dialog.prop('offsetLeft');
startY = modalInstance.dialog.prop('offsetTop');
//Get the initiali mouse click
initialMouseX = $event.clientX;
initialMouseY = $event.clientY;
$document.bind('mousemove', mousemove);
$document.bind('mouseup', mouseup);
return false;
});
To move the dialog around the screen. The return false tells the event not to propagate further.
The position is calculated based on the starting position, the mouse position (clientX and clientY)
and the first position of the mouse click
var mousemove = function($event) {
var dx = startX + ($event.clientX - initialMouseX);
var dy = startY + ($event.clientY - initialMouseY);
var style = {
top: dy + 'px',
left: dx + 'px'
};
//Force the css
modalInstance.dialog.css(style);
return false;
}
At the end the mouse move and up are unbinded.
var mouseup = function() {
$document.unbind('mousemove', mousemove);
$document.unbind('mouseup', mouseup);
}
Changing the dialog content controllers
First remove all the reference in $routeProvider to other than the list and the default route.
Into the list controller we will add the dependency on globalModalService.
We specify then how to open the dialog adding the calls to this functions on the customers/list.html.
The parameters for the openModal are:
- template: The template that will be loaded inside the dialog
- data: A javascript object that will be added on the controller scope
- callback: Function that will be called by the dialog when closing it.
- controller: (optional) If specified it overrides the ng-controller on the template.
$scope.editDetail = function(id){
globalModalService.openModal(
{
template:'app/customers/edit.html',
data:{id:id},
callback: function(){ $scope.loadData();}
})
}
$scope.addNew = function(){...}
$scope.viewDetail = function(id){...}
Let see the edit detail controller.
We add the dependency from modalInstance, that exposes the "closeModal" method.
The changes of location towards the list of customers will be changed to close the
modal
modalInstance.closeModal();
The parameter are not passed anymore as route parameters
$http.get('/customers/'+$routeParams.customerId)
is replaced, looking at what is passed by the openModal in the data field
$http.get('/customers/'+$scope.id)
Finally we will add the buttons and the title.
The button have the following specs
- action: The function that will be executed on click.
- text: The text inside the button
- class: The css class added to the button
- disabled: The function to check if the button is disabled
Note specifically that we use the $scope.editForm. That is the form defined inside the
edit.html template!
$scope.title = "Editing customer "+$scope.id;
$scope.modalButtons =[
{
action:function(){modalInstance.closeModal();},
text:"Cancel",class:"btn-default"
},
{
action:function(){update();},
text:"Update",class:"btn-primary",
disabled: function(){ if($scope.editForm)return $scope.editForm.$invalid || !$scope.editForm.$dirty;}
}
];
Changing the dialog content template
We will use exactly the same template used on the point 5 simply adding around the form a div that
declares the ng-controller that will be used by the template!
<div ng-controller="CustomerEditController"> <form novalidate name="editForm" id="edit-form" role="form" ng-submit="update()"> ... </form> </div>