New version Angular 9
In this part we will discover how to build a dialog service.
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 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"!!
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>
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; }
We will create for this a service, called "globalModalService". The dependencies will be:
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:
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' }); }
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); }
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:
$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
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;} } ];
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>