Modal dialogs are designed to bring important pieces of information to users from any page without changing an active
routing state. It is often convenient to utilize a modal dialog that itself contains a number of views and its
own routing. And, since the $ionicModal
service does not provide this functionality, here is where custom modal implementations come into play.
This post shows how to create a custom modal dialog that provides the ability to navigate between its inner views.
The most common example of the modal with inner navigation is the 'info' page which contains a list of sub-menus that
break content down into categories.
In order to implement the 'info' modal, we will create two services. The first service will provide a generic modal
implementation with methods for hiding and showing the modal dialog. And the second service will represent a specific
modal that allows to create a routing configuration with inner views. Both services will be designed to work in
conjunction with their companion directives for embedding modals into page layouts.
/** * Helps to access and manipulate custom modal instances. */.factory('customModal',['$ionicPlatform','$ionicHistory','$state','$timeout',function($ionicPlatform,$ionicHistory,$state,$timeout){// container to hold all available modal instancesvarmodals=[];// registering hardware `back` button handler...registerBackButtonAction();return{// returns a modal instance by idget:get,// attaches a directive handler which allows to display / hide the modalsetHandler:setHandler};////////////////////////////// *** Implementation *** ///////////////////////////////** * Intercepts the hardware `back` button click on a mobile device. */functionregisterBackButtonAction(){// registers back button action which closes the modal if it is openedvarpriority=500;// the highest priority for the action, please read// http://ionicframework.com/docs/api/service/$ionicPlatform/$ionicPlatform.registerBackButtonAction(backButtonAction,priority);// closes the modal if it is opened, otherwise executes 'go back' action.functionbackButtonAction(){// checks if there is a modal that is currently openedvarmodal=modals.find(function(modal){returnmodal&&modal.directiveHandler&&!modal.directiveHandler.isHidden();});if(modal){// closes the modal view if it is openedmodal.close();// simulates state change in order to trigger the modal hiding$state.go($state.current.name);}else{// otherwise, checks if there is a way backif($ionicHistory.viewHistory().currentView.backViewId===null){// exists the app if there is no way backionic.Platform.exitApp();}else{// or goes back to the previous page$ionicHistory.goBack();}}}}/** * Returns a modal instance by id. */functionget(id){returnmodals.find(function(modal){returnmodal.id===id;})||createModal(id);}/** * Attaches a directive handler which is used to display / hide the modal. */functionsetHandler(id,handler){get(id).directiveHandler=handler;}/** * Creates a new modal instance. */functioncreateModal(id){varmodal={// unique modal identifierid:id,// a set of dummy callback functions which can be defined in a controllercallbacks:['beforeOpened','afterOpened','beforeClosed','afterClosed'].reduce(function(result,item){result[item]=function(){};returnresult;},{}),// shows the modalshow:show,// hides the modalclose:close};// adds modal to the array with the other modals.modals.push(modal);returnmodal;/** * Triggers the 'open' event, and executes callbacks. */functionshow(){this.callbacks.beforeOpened();this.directiveHandler.show();this.callbacks.afterOpened();}/** * Triggers the 'close' event, and executes callbacks. */functionclose(){this.callbacks.beforeClosed();this.directiveHandler.close();// wait till window is closed, and only then perform DOM manipulations$timeout(this.callbacks.afterClosed,500);}}}]);
Two main things that the customModal service does are:
provides custom modal instances to the user to show / hide a modal by id
processes the hardware 'back` button click in order to close an active modal
To get a modal instance in a controller, the get function should be invoked (line 15). That function requires only
the id parameter which should match the 'id' of the directive (will be described in the next section). Every modal
instance provides show() and close() methods and a set of dummy callback functions callbacks that intended to be
defined in a controller.
1.2 The directive
The best approach here would be to design the directive that automatically registers its visibility handler during the
initialization using the setHandler method in the custom modal service:
/*** Renders modal template.*/.directive('customModal',['customModal','$compile',function(customModal,$compile){return{restrict:'E',transclude:true,link:link};////////////////////////////// *** Implementation *** //////////////////////////////functionlink(scope,element,attrs,ctrl,transclude){// gets the directive idvarid=attrs.id;// creates a unique variable name for `ng-hide`varngHideBinder="hidden_"+id;varmodalEl='<ion-pane ng-hide="'+ngHideBinder+'" class="menu-animation ng-hide"></ion-pane>';scope[ngHideBinder]=true;// gets the directive content and appends it to a modal elementtransclude(function(clone){varwrapper=$compile(modalEl)(scope);element.append(wrapper.append(clone));});// registers directive handler in the customModal servicecustomModal.setHandler(id,handler());//////////////////////////////////// object that provides methods for the modal visibility handlingfunctionhandler(){return{show:show,close:close,isHidden:isHidden};// shows the modalfunctionshow(){scope[ngHideBinder]=false;}// hides the modalfunctionclose(){scope[ngHideBinder]=true;}// checks if the modal is hidden / visiblefunctionisHidden(){returnscope[ngHideBinder];}}}}]);
The implementation of the directive above is quite simple — the handler function provides an object with methods
for hiding and showing the modal and is being passed to the customModal service.
At this point, we already have all the functionality in place for creating basic modal dialogs. For instance, to create
a custom modal in your app, you can add the custom-modal directive to the html page and then refer to its instance
from a controller using the customModal service and the modal element id:
file.html
1234
<!-- the directive should be placed next to the app container element. --><custom-modalid="modal1"> Modal content goes here...
</custom-modal>
Now, when the simple custom modal is implemented, let's move ahead and build an extended version that will provide us
with the basic inner routing. The service for manipulating modal state will be called multiViewModal and will use
the customModal implementation in order to provide basic modal capabilities:
/** * Helps to access and manipulate navigatable modal instances with * multiple inner views. */.factory('multiViewModal',['customModal',function(customModal){// container to hold all available modal instancesvarmodals=[];// the methods defined bellow partially replicate the methods of 'custom modal' service// you might consider moving them to a 'base modal' service for a real-life applicationreturn{get:get,setHandler:setHandler};////////////////////////////// *** Implementation *** ///////////////////////////////** * Returns a modal instance by id. */functionget(id){returnmodals.find(function(modal){returnmodal.baseModal.id===id;})||createModal(id);}/** * Attaches a directive handler which allows to manipulate modal state. */functionsetHandler(id,handler){get(id).directiveHandler=handler;}/** * Creates a new modal instance. */functioncreateModal(id){varmodal={show:show,close:close,baseModal:customModal.get(id),// activates view with the given nameactivateView:activateView,// activates the previous view in hierarchypreviousView:previousView};modal.baseModal.callbacks.afterClosed=afterClosed(modal);// adds modal to the array with the other modals.modals.push(modal);returnmodal;/** * Triggers the 'open' event. */functionshow(){this.baseModal.show();}/** * Triggers the 'close' event. */functionclose(){this.baseModal.close();}/** * Activates view with the given name. */functionactivateView(name){this.directiveHandler.activateView(name);}/** * Activates the previous view in hierarchy. */functionpreviousView(){this.directiveHandler.previousView();}/** * Clears inputs and pre-activates the root view if required. */functionafterClosed(modal){varm=modal;returnfunction(){varhandler=m.directiveHandler;// `erasable` determines if all input data is erased after the modal is closedif(handler.options.erasable)handler.clearInputs();// `returnable` determines if the root view should be displayed to the user// when the modal is opened for the next timeif(handler.options.returnable)handler.activateRoot();};}}}]);
The multiViewModal service has the same structure as the customModal:
The get method returns a multi view modal instance by id (line 11). The visibility
control is performed via show and close methods of customModal. Also, the multiViewModal service adds two
more functions, called activateView and previousView. Those functions are responsible for the routing between views
(how the views are initialized will be shown shortly).
The setHandler method assigns directive handler to the modal instance with an appropriate 'id'.
2.2 The directive
The next code snippet contains a directive called multi-view-modal that is used in conjunction with the multiViewModal
service:
.directive('multiViewModal',['multiViewModal','$compile',function(multiViewModal,$compile){return{restrict:'E',link:link};////////////////////////////// *** Implementation *** //////////////////////////////functionlink(scope,element,attrs){varid=attrs.id;varviews='view_'+id;// reads views defined as child directive elementsscope[views]=readViews(element.children());// sets 'isActive' flag for the root viewscope[views].find(function(view){returnview.root;}).isActive=true;// reads options defined as directive attributesvaroptions={// controls if all data should be erased after a modal is closederasable:attrs.erasable?attrs.erasable==='true':true,// controls if the current active view will remain active when the modal reopenedreturnable:attrs.returnable?attrs.returnable==='true':true};// initializes and updates directive templatevarviewsTemplate='<ion-pane ng-repeat="item in '+views+'"'+' ng-show="item.isActive" ng-include="item.url"></ion-pane>';varbaseModalTemplate='<custom-modal id="'+id+'">'+viewsTemplate+'</custom-modal>';varbaseModal=$compile(baseModalTemplate)(scope);element.replaceWith(baseModal);// registers directive handler in the multiViewModal servicemultiViewModal.setHandler(id,handler());/** * Retrieves an information about the views * from the directive child elements and their attrs. */functionreadViews(childElements){returnArray.from(childElements).reduce(function(views,viewItem){if(viewItem.localName==='view-item'){views.push(['name','url','root','parent'].reduce(function(view,attrName){varattribute=viewItem.attributes[attrName];if(attribute){varvalue=attribute.value;view[attrName]=value==='true'||value==='false'?value==='true':value;}returnview;},{}));}returnviews;},[]);}/** * Creates a handler to manipulate the directive state. */functionhandler(){return{options:options,activateRoot:activateRoot,// activates the root viewactivateView:activateView,// activates view with the given namepreviousView:previousView,// activates the previous view in hierarchyclearInputs:clearInputs// clears all inputs by recompiling the modal};// sets view with an appropriate name as active.functionactivateView(name){scope[views].forEach(function(view){if(view.name===name)view.isActive=true;elsedeleteview.isActive;});}// activates the root viewfunctionactivateRoot(){activateView(scope[views].find(function(view){returnview.root;}).name);}// goes back to the previous view.functionpreviousView(){activateView(scope[views].find(function(view){returnview.isActive;}).parent);}// clears inputs by recompiling the modalfunctionclearInputs(){varpane=angular.element(baseModal.children()[0]);pane.empty();pane.append($compile(viewsTemplate)(scope));}}}}]);
The multi-view-modal directive creates the views template, initializes its handler object, and then passes
the handler to the multiViewModal service.
To create a modal with inner navigation, you need to add the <multi-view-modal> directive
next to the app's container element and invoke the get method in the controller:
file.html
123456
<!-- The directive should be placed next to the app container element --><multi-view-modalid="modal2"erasable="false"returnable="true"><view-itemname="home"url="templates/info-home.html"root="true"/><view-itemname="section1"url="templates/info-section-1.html"parent="home"/><view-itemname="section2"url="templates/info-section-2.html"parent="home"/></multi-view-modal>
The <view-item> elements, that is being wrapped in the <multi-view-modal> directive, should describe the separate
view elements and their routing position relative to the others:
name: unique view name;
url: link to the html template for this view;
root: boolean parameter which indicates that the page is a root element, i.e. shown to the user
right after the modal is opened;
parent: name of the view that should be opened when the back button is clicked (should be
defined for all the views except the root).
The examples, that show how these services can be used in the the app, are available on
Plunker and
GitHub.
If you're looking for a developer or considering starting a new project,
we are always ready to help!