3

I'm using UI Bootstrap's $uibModal to create a modal. I'm also using UI Router 0.2.15, so what I want is a state opening in a new modal.

This is what I have in my config function:

    $stateProvider
    .state("mystate.substate1", {
        url: '...',
        template: '<div ui-view></div>',
        onEnter: showFirstCustomModal
    })
    .state("mystate.substate2", {
        url: '...',
        onEnter: showSecondCustomModal
    });

    // End of calling code

    function showFirstCustomModal($uibModal) {

        var options = {
            backdrop: 'static',
            templateUrl: '...',
            controller: 'Controller1',
            controllerAs: 'controller'
        };

        $uibModal.open(options);
    }

    function showSecondCustomModal($uibModal) {

        var options = {
            backdrop: 'static',
            templateUrl: '...',
            controller: 'Controller2',
        };

        $uibModal.open(options);
    }

The two modal methods above are very similar. I would like to replace them with a generic method:

    $stateProvider
    .state("mystate.substate1", {
        url: '...',
        onEnter: showGenericModal('some_template','SomeController1', 'alias1')
    })
    .state("mystate.substate2", {
        url: '...',
        onEnter: showGenericModal('some_other_template', 'SomeController2')
    });

    // End of calling code

    function showGenericModal(templateUrl, controller, controllerAlias, $uibModal) {

        var options = {
            backdrop: 'static',
            templateUrl: templateUrl,
            controller: controller
        };

        if(!!controllerAlias) {
            options.controllerAs: controllerAlias;
        }

        $uibModal.open(options);
    }

I put the $uibModal as the last argument to avoid it getting reassigned. But I can't get this to work. The error I get is

 Cannot read property 'open' of undefined

Also, I've been reading this and I know that you'll have to use the $injector in order to allow your service to be injected. But I supposed that's already handled by UI-Bootstrap.

Community
  • 1
  • 1
cst1992
  • 3,823
  • 1
  • 29
  • 40

2 Answers2

1

Since $stateProvider is defined in config block, $uibModal can't be passed from there as a reference.

It is not possible to mix dependencies and normal arguments in Angular DI. For onEnter it should be a function that accepts the list of dependencies.

The code above translates to:

onEnter: showGenericModal('some_other_template', 'SomeController2')

...

function showGenericModal(templateUrl, controller, controllerAlias) {
  return ['$uibModal', function ($uibModal) {
    ...
    $uibModal.open(options);
  }];
}

Or a better approach:

onEnter: function (genericModal) {
  genericModal.show('some_other_template', 'SomeController2');
}

...

app.service('genericModal', function ($uibModal) {
  this.show = function (templateUrl, controller, controllerAlias) {
    ...
    $uibModal.open(options);
  }
});
Estus Flask
  • 206,104
  • 70
  • 425
  • 565
  • You're welcome. Both should work, in fact. There was a typo, the problem with showGenericModal is that it shouldn't be called. – Estus Flask Jan 21 '17 at 12:13
  • I think one should be using `$injector.invoke()` on the first one. I'm no expert on this one though. – cst1992 Jan 21 '17 at 12:37
  • @cst1992 `onEnter` function is called with `$injector.invoke` under the hood. That's why it is necessary to return another function from `showGenericModal(templateUrl, controller, controllerAlias)` call, like it was done in the first snippet. – Estus Flask Jan 21 '17 at 12:46
1

@estus answer correct, I don't know how I didn't saw the state: "For onEnter it should be a function that accepts the list of dependencies.".

However, I will let my answer here to provide another perspective. You can define a service to wrap up and organize correctly your code, in order to call a customized modal on onEnter state event:

angular.module('app').service('AppModals', AppModals);

// or use /** @ngInject */ aswell
AppModals.$inject = ['$uibModal'];

function AppModals($uibModal) {
    this.open = function _generateModal(options) {
        var defaultOptions = {
            backdrop: 'static'
            // Any other default option
        };

        return $uibModal.open(angular.extend({}, defaultOptions, options);
    };
}

On the state definition:

$stateProvider
    .state('app.state', {
        url: '/state-modal',
        template: '<ui-view></ui-view>',
        controller: 'DummyCtrl',
        controllerAs: 'dummy',
        onEnter: appState_onEnter
    });

// or use /** @ngInject */ aswell
appState_onEnter.$inject = ['$uibModal'];

function appState_onEnter(AppModals) {
    AppModals.open({
        templateUrl: 'modals/state-modal.html',
        controller: 'DummyCtrl',
        controllerAs: 'dummy'
    });
}
Mateus Leon
  • 1,381
  • 1
  • 14
  • 21
  • I think @estus means that it is not possible to mix and match injectable and non-injectable arguments in Angular DI. If a service is created, it is made injectable, so that it can then be used as an argument to the `onEnter` function. – cst1992 Jan 21 '17 at 06:20
  • BTW, I think your injection on appState_onEnter is wrong; it should be `['AppModals']`. – cst1992 Jan 21 '17 at 07:21
  • It wasn't erroneous information. DI isn't used here `showGenericModal('some_other_template', 'SomeController2')`, the function is just called from parent function (`config` block), with $uibModal param being omitted. That's why it doesn't cause injection error. If it were some other function scope, it would be possible to pass $uibModal from there, but it's not the case for $stateProvider. – Estus Flask Jan 21 '17 at 12:06
  • @estus How is `$uibModal` injected in the `onEnter` function? – cst1992 Jan 21 '17 at 12:39
  • @estus is an erroneous information, you are not able to see in your code that you define a function to simply return an annotated array? If you declare the array directly the result is the same. From their docs "Callback function for when a state is entered. Good way to trigger an action or dispatch an event, such as opening a dialog. If minifying your scripts, make sure to explicitly annotate this function, because it won't be automatically annotated by your build tools.". Is it clear now? – Mateus Leon Jan 23 '17 at 13:22
  • I'm not sure what's your point, but you've got the answer wrong and made the erroneous assumption. `onEnter: showGenericModal('some_other_template', 'SomeController2')` **doesn't** use DI (this is function call) and successfully passes parameters to another annotated function, which **does** use DI. It is not possible to do this with a single function `showGenericModal(templateUrl, controller, controllerAlias, $uibModal) { ... }`, like OP tried. That's what the answer says. – Estus Flask Jan 23 '17 at 13:35
  • @estus, I've updated my answer, I really didn't noticed that you already said that on your anwser, I'm sorry to let this confusion take form. – Mateus Leon Jan 23 '17 at 13:37
  • @estus hahahahah, man... you were completely right on your answer, but not now on your comment, where you did made the assumption that I've warned as erroneous. Please, see on this plunkr the use of `onEnter` with an annotated array directly on the object key value: https://plnkr.co/edit/Y3s0WmUF1NDEiJ8ZmGuU – Mateus Leon Jan 23 '17 at 14:05
  • Sure, no problem. – Estus Flask Jan 23 '17 at 14:10
  • In your example it is classic DI, which is supported everywhere in Angular in totally similar manner (because all DI-enabled functions are called with $injector.invoke under the hood), no extra parameters are passed. But you can't 'mix injected dependencies with custom arguments' there, as the question requires. – Estus Flask Jan 23 '17 at 14:13
  • @estus Now I feel that using a service is cleaner anyway. – cst1992 Jan 23 '17 at 14:33
  • @cst1992 I would personally go this way, a service is more testable (and possibly reusable). – Estus Flask Jan 23 '17 at 14:41