Ionic modal service with extras

Based on this solution: Ionic modal windows from service and the fact that it needed a more practical way to create and display modals with some features below:

  • Pass parameters;
  • Set an independent controller;
  • Get results after completing / close the modal;
  • Use promises to get the result;
  • Isolate the scope of the modal;
  • Release the resource when not using it;

I created this little service that I think can be useful…

CodePen

(function () {
'use strict';

var serviceId = 'appModalService';
angular.module('app').factory(serviceId, [
    '$ionicModal', '$rootScope', '$q', '$injector', '$controller', appModalService
]);

function appModalService($ionicModal, $rootScope, $q, $injector, $controller) {

    return {
        show: show
    }

    function show(templateUrl, controller, parameters) {
        // Grab the injector and create a new scope
        var deferred = $q.defer(),
            ctrlInstance,
            modalScope = $rootScope.$new(),
            thisScopeId = modalScope.$id;

        $ionicModal.fromTemplateUrl(templateUrl, {
            scope: modalScope,
            animation: 'slide-in-up'
        }).then(function (modal) {
            modalScope.modal = modal;

            modalScope.openModal = function () {
                modalScope.modal.show();
            };
            modalScope.closeModal = function (result) {
                deferred.resolve(result);
                modalScope.modal.hide();
            };
            modalScope.$on('modal.hidden', function (thisModal) {
                if (thisModal.currentScope) {
                    var modalScopeId = thisModal.currentScope.$id;
                    if (thisScopeId === modalScopeId) {
                        deferred.resolve(null);
                        _cleanup(thisModal.currentScope);
                    }
                }
            });

            // Invoke the controller
            var locals = { '$scope': modalScope, 'parameters': parameters };
            var ctrlEval = _evalController(controller);
            ctrlInstance = $controller(controller, locals);
            if (ctrlEval.isControllerAs) {
                ctrlInstance.openModal = modalScope.openModal;
                ctrlInstance.closeModal = modalScope.closeModal;
            }

            modalScope.modal.show();

        }, function (err) {
            deferred.reject(err);
        });

        return deferred.promise;
    }

    function _cleanup(scope) {
        scope.$destroy();
        if (scope.modal) {
            scope.modal.remove();
        }
    }

    function _evalController(ctrlName) {
        var result = {
            isControllerAs: false,
            controllerName: '',
            propName: ''
        };
        var fragments = (ctrlName || '').trim().split(/\s+/);
        result.isControllerAs = fragments.length === 3 && (fragments[1] || '').toLowerCase() === 'as';
        if (result.isControllerAs) {
            result.controllerName = fragments[0];
            result.propName = fragments[2];
        } else {
            result.controllerName = ctrlName;
        }

        return result;
    }


} // end
})();

To use:

appModalService
.show('<templateUrl>', '<controllerName> or <controllerName as ..>', <parameters obj>)
.then(function(result) {
    // result
}, function(err) {
    // error
});

In controller is possible to obtain the parameters injecting ‘parameters’ and close passing the result for ‘closeModal (result)’;

Can use another service to centralize the configuration of all modals:

angular.module('app')
.factory('myModals', ['appModalService', function (appModalService){

var service = {
    showLogin: showLogin,
    showEditUser: showEditUser
};

function showLogin(userInfo){
    // return promise resolved by '$scope.closeModal(data)'
    // Use:
    // myModals.showLogin(userParameters) // get this inject 'parameters' on 'loginModalCtrl'
    //  .then(function (result) {
    //      // result from closeModal parameter
    //  });
    return appModalService.show('templates/modals/login.html', 'loginModalCtrl as vm', userInfo)
    // or not 'as controller'
    // return appModalService.show('templates/modals/login.html', 'loginModalCtrl', userInfo)
}

function showEditUser(dataParams){
    // return appModalService....
}

}]);
12 Likes

Thanks for sharing, the only con I see using this service is that, the modal is removed from the DOM every time the modal is hidden.

Any solutions for that?

thanks

Yes indeed. However I see it positively in most of my use cases. It frees the resources of the digest and excesses in the DOM. Not to I have noticed no performance improvement when kept my modal cached and loaded into the DOM.

1 Like

Thanks a lot for sharing ! This is really useful !

Awesome ! Thanks, this helped me a lot !

THX. This is exactly what I needed!

Thanks a lot. This is really helpful.

Hey Julian - thanks for sharing this. I’m leveraging a controller which originally had resolve statements to populate a few variables before instantiating the view. How would I best recreate the resolve functionality (wait until promises complete and pass to controller) with this approach in instantiating modals with a controller?

I thought the parameters hash would be the key to doing this but just need a simple example to get me over them hump :smile:

Thanks!

Thanks for the service, but I’m facing one problem with this. I’m unable to use ‘controller as property’ way of accessing. The ‘property’ is not be available in the modal template. But I’m able to access any function / variable attached to the ‘$scope’ in there. I’ve rechecked the way of declaring and accessing the ‘property’, and it is good (working at all other places). Also the ‘property’ is available in the ‘$scope’ object when checked on the console dump (firebug). As I can see, in the example (codepen), it is working good.
So, do anyone have input on where I might have gone wrong ?

I’m using this service it’s very useful.
But I faced with problem.
I can’t inject $ionicScrollDelegate to the assigned controller.
$ionicScrollDelegate.$getByHandle(‘mainScroll’).scrollTop()
raise the error:

Delegate for handle “mainScroll” could not find a corresponding element with delegate-handle=“mainScroll”! scrollTop() was not called!
Possible cause: If you are calling scrollTop() immediately, and your element with delegate-handle=“mainScroll” is a child of your controller, then your element may not be compiled yet. Put a $timeout around your call to scrollTop() and try again.

Even if I use it within $timeout

Same for me with $ionicSlideBoxDelegate. I can’t use $ionicSlideBoxDelegate within the modal’s controller.
$ionicSlideBoxDelegate.currentIndex() etc. is undefined…

Any suggestions how to use the delegates within the modal?

Same Issue in my cases - Delegates won’t work.
I debugged some, an i think the modal ist the active scope so the delegate-handler doesn’t work.
In a single View Application the Modals work with Delegates but with Menu not …

Thank you for sharing…that’s exactly what I need.

@julianpaulozzi I love you :smile:

How to use the parameters in the modal controller !

Unable to get this modal service to work with forms.
I’ve created a codepen to show the problem.

When used with this service, the vm object is directly replaced with the FormController object, so none of the attributes of vm is accessible in the template and the controller associated with the vm object becomes totally useless.

I’ve added the code here for reference. Any suggestions on fixing this issue ?

HTML:

<html ng-app="ionicApp">

<head>
  <meta charset="utf-8">
  <meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no, width=device-width">

  <title>Ionic modal service</title>

  <link href="http://code.ionicframework.com/nightly/css/ionic.css" rel="stylesheet">
  <script src="http://code.ionicframework.com/nightly/js/ionic.bundle.js"></script>

</head>

<body ng-controller="AppCtrl as vm">

  <ion-content padding="true" ng-class="{'has-footer': showFooter}">
    <div class="row">
      <div class="col">
        <button ng-click="vm.showNonWorkingForm()" class="button button-assertive button-block">
          Show Non Working Form (using appModalService)
        </button>
      </div>
    </div>

    <div class="row">
      <div class="col">
        <button ng-click="vm.showWorkingForm()" class="button button-assertive button-block">
          Show Working Form (using ionic ModalService)
        </button>
      </div>
    </div>

  </ion-content>

  <script id="non-working-form-modal.html" type="text/ng-template">
    <ion-modal-view>
      <ion-header-bar class="bar bar-header bar-positive">
        <h1 class="title">Form</h1>
        <button class="button button-clear button-primary" ng-click="vm.closeModal()">Close</button>
      </ion-header-bar>
      <ion-content>
        <form novalidate name="vm.f" ng-submit="vm.submit()">
          <div class="list">
            <label class="item item-input">
              <input placeholder="text" type="text" name="sometext" ng-model="vm.sometext" required>
            </label>
          </div>
          <button type="submit" class="button button-block">
            Submit
          </button>
        </form>
        {{ vm }}
      </ion-content>
    </ion-modal-view>
  </script>

  <script id="working-form-modal.html" type="text/ng-template">
    <div class="modal" ng-controller="WorkingCtrl as vm">
      <ion-header-bar class="bar bar-header bar-positive">
        <h1 class="title">Form</h1>
        <button class="button button-clear button-primary" ng-click="closeModal()">Close</button>
      </ion-header-bar>
      <ion-content>
        <form novalidate name="vm.f" ng-submit="vm.submit()">
          <div class="list">
            <label class="item item-input">
              <input placeholder="text" type="text" name="sometext" ng-model="vm.sometext" required>
            </label>
          </div>
          <button type="submit" class="button button-block">
            Submit
          </button>
        </form>
        {{ vm }}
      </ion-content>
    </div>
  </script>
  
</body>

</html>

JS

angular.module('ionicApp', ['ionic'])
  .controller('NonWorkingCtrl', ['$scope', 'parameters', function($scope, parameters) {
    var vm = this;
    /* placeholder for the FormController object */
    vm.f = null;
    vm.sometext = 'Added Some text';
    vm.submit = function() {
        if (vm.f.$valid) {
          alert('NonWorkingCtrl Valid');
        } else {
          alert('NonWorkingCtrl InValid');
        }
      }
      /* additional fields */
    vm.field1 = 'field1';
    vm.field2 = 'field2';
    vm.field3 = 'field3';
    vm.field4 = 'field4';
    vm.field5 = 'field5';
    vm.field6 = 'field6';
    vm.field7 = 'field7';
  }])
  .controller('WorkingCtrl', ['$scope', function($scope) {
    var vm = this;
    /* placeholder for the FormController object */
    vm.f = null;
    vm.sometext = 'Added Some text';
    vm.submit = function() {
        if (vm.f.$valid) {
          alert('WorkingCtrl Valid');
        } else {
          alert('WorkingCtrl InValid');
        }
      }
      /* additional fields */
    vm.field1 = 'field1';
    vm.field2 = 'field2';
    vm.field3 = 'field3';
    vm.field4 = 'field4';
    vm.field5 = 'field5';
    vm.field6 = 'field6';
    vm.field7 = 'field7';
  }])
  .controller('AppCtrl', ['$scope', 'appModalService', '$ionicModal', function($scope, appModalService, $ionicModal) {
    var vm = this;
    vm.showNonWorkingForm = function() {
      appModalService.show('non-working-form-modal.html', 'NonWorkingCtrl as vm');
    };

    vm.showWorkingForm = function() {
      $ionicModal.fromTemplateUrl('working-form-modal.html', {
        scope: $scope,
        animation: 'slide-in-up'
      }).then(function(modal) {
        $scope.modal = modal;
        $scope.modal.show();
      });
      $scope.closeModal = function() {
        $scope.modal.hide();
        $scope.modal.remove();
      };
    }
  }])
  .factory('appModalService', ['$ionicModal', '$rootScope', '$q', '$injector', '$controller', function($ionicModal, $rootScope, $q, $injector, $controller) {

    return {
      show: show
    }

    function show(templeteUrl, controller, parameters, options) {
      // Grab the injector and create a new scope
      var deferred = $q.defer(),
        ctrlInstance,
        modalScope = $rootScope.$new(),
        thisScopeId = modalScope.$id,
        defaultOptions = {
          animation: 'slide-in-up',
          focusFirstInput: false,
          backdropClickToClose: true,
          hardwareBackButtonClose: true,
          modalCallback: null
        };

      options = angular.extend({}, defaultOptions, options);

      $ionicModal.fromTemplateUrl(templeteUrl, {
        scope: modalScope,
        animation: options.animation,
        focusFirstInput: options.focusFirstInput,
        backdropClickToClose: options.backdropClickToClose,
        hardwareBackButtonClose: options.hardwareBackButtonClose
      }).then(function(modal) {
        modalScope.modal = modal;

        modalScope.openModal = function() {
          modalScope.modal.show();
        };
        modalScope.closeModal = function(result) {
          deferred.resolve(result);
          modalScope.modal.hide();
        };
        modalScope.$on('modal.hidden', function(thisModal) {
          if (thisModal.currentScope) {
            var modalScopeId = thisModal.currentScope.$id;
            if (thisScopeId === modalScopeId) {
              deferred.resolve(null);
              _cleanup(thisModal.currentScope);
            }
          }
        });

        // Invoke the controller
        var locals = {
          '$scope': modalScope,
          'parameters': parameters
        };
        var ctrlEval = _evalController(controller);
        ctrlInstance = $controller(controller, locals);
        if (ctrlEval.isControllerAs) {
          ctrlInstance.openModal = modalScope.openModal;
          ctrlInstance.closeModal = modalScope.closeModal;
        }

        modalScope.modal.show()
          .then(function() {
            modalScope.$broadcast('modal.afterShow', modalScope.modal);
          });

        if (angular.isFunction(options.modalCallback)) {
          options.modalCallback(modal);
        }

      }, function(err) {
        deferred.reject(err);
      });

      return deferred.promise;
    }

    function _cleanup(scope) {
      scope.$destroy();
      if (scope.modal) {
        scope.modal.remove();
      }
    }

    function _evalController(ctrlName) {
      var result = {
        isControllerAs: false,
        controllerName: '',
        propName: ''
      };
      var fragments = (ctrlName || '').trim().split(/\s+/);
      result.isControllerAs = fragments.length === 3 && (fragments[1] || '').toLowerCase() === 'as';
      if (result.isControllerAs) {
        result.controllerName = fragments[0];
        result.propName = fragments[2];
      } else {
        result.controllerName = ctrlName;
      }

      return result;
    }

  }]);

PS: I definitely need the access of the form object in the controller(vm) to handle the errors returned from the server (think of the authentication failure responses from the server).

Solved the issue with

Inside your model controller

var vm = $scope;

instead of

var vm = this

setup your model in the controller

vm.customerObject = {
name: ‘customer name’
};

and refer the above model in the html directly like this

<input type="text" name="Name" placeholder="Name" ng-model="customerObject.name" ng-model-options="{updateOn: 'blur'}"  ng-minlength="5" ng-maxlength="150" required>

[Note: vm.customerObject.name does’t work hence referring it directly]

my model controller looks like this:

'use strict';
export class ProjectController {

constructor($scope, logger, parameters, projectService) {
‘ngInject’;
var vm = $scope;

I posted an alternative solution here for those who stumble on this.

Ok, I have seen a lot of different solutions to better handling Ionic modals because of the lack of a controller option or something similar.
After playing with React for a while I came up with another option, more declarative in my opinion. Is in ES6 and just a prototype but you can have an idea:

(function() {
  'use strict';

  @Inject('$scope', '$ionicModal', '$transclude', '$rootScope')
  class Modal {
    constructor() {
      let { animation, focusFirstInput, backdropClickToClose, hardwareBackButtonClose } = this;
      $transclude((clone, scope) => {
        let modal = this.createModalAndAppendClone({
          scope,
          animation,
          focusFirstInput,
          backdropClickToClose,
          hardwareBackButtonClose
        }, clone);
        this.setupScopeListeners(modal.scope);
        this.createIsOpenWatcher();
        this.addOnDestroyListener();
        this.emitOnSetupEvent(modal.scope);
      });
    }
    setupScopeListeners(scope) {
      scope.$on('modal.shown', this.onShown);
      scope.$on('modal.hidden', this.onHidden);
      scope.$on('modal.removed', this.onRemoved);
    }
    addOnDestroyListener() {
      this.$scope.$on('$destroy', () => {
        this.removeModal();
      });
    }
    createIsOpenWatcher() {
      this.isOpenWatcher = this.$scope.$watch(() => this.isOpen, () => {
        if (this.isOpen) {
          this.modal.show();
        } else {
          this.modal.hide();
        }
      });
    }
    emitOnSetupEvent(scope) {
      this.onSetup({
        $scope: scope,
        $removeModal: this.removeModal.bind(this)
      });
    }
    createModalAndAppendClone({
      scope = this.$rootScope.$new(true),
      animation = 'slide-in-up',
      focusFirstInput = false,
      backdropClickToClose = true,
      hardwareBackButtonClose = true
    }, clone) {
      let options = {
        scope,
        animation,
        focusFirstInput,
        backdropClickToClose,
        hardwareBackButtonClose
      }
      this.modal = this.$ionicModal.fromTemplate('<ion-modal-view></ion-modal-view>', options);
      let $modalEl = angular.element(this.modal.modalEl);
      $modalEl.append(clone);
      return this.modal;
    }
    removeModal() {
      this.modal.remove();
      this.isOpenWatcher();
    }
  }

  function modal() {
    return {
      restrict: 'E',
      transclude: true,
      scope: {
        'onShown': '&',
        'onHidden': '&',
        'onRemoved': '&',
        'onSetup': '&',
        'isOpen': '=',
        'animation': '@',
        'focusFirstInput': '=',
        'backdropClickToClose': '=',
        'hardwareBackButtonClose': '='
      },
      controller: Modal,
      bindToController: true,
      controllerAs: 'vm'
    }
  }
  
  angular
    .module('flight')
    .directive('modal', modal);

})();

And then you can use it like this:

<modal is-open="vm.isOpen" on-shown="vm.onShown()" on-hidden="vm.onHidden()" on-removed="vm.onRemoved()" on-setup="vm.onSetup($scope, $removeModal)">
  <div class="bar bar-header bar-clear">
    <div class="button-header">
      <button class="button button-positive button-clear button-icon ion-close-round button-header icon" ng-click="vm.closeModal()"></button>
    </div>
  </div>
  <ion-content class="has-header">
    <create-flight-form on-submit="vm.submit()"></create-flight-form>
  </ion-content>
</modal>

You open and close the modal with a boolean value bind to is-open and then register callbacks for the different events.