31

I know this has been covered many times and most articles refer to this bit of code: Modal window with custom URL in AngularJS

But I just don't get it. I don't find that to be very clear at all. I also found this jsfiddle which was actually great, very helpful except this doesn't add the url and allow for me to use the back button to close the modal.


Edit: This is what I need help with.

So let me try explain what I am trying to achieve. I have a form to add a new item, and I have a link 'add new item'. I would like when I click 'add new item' a modal pops up with the form I have created 'add-item.html'. This is a new state so the url changes to /add-item. I can fill out the form and then choose to save or close. Close, closes the modal :p (how odd) . But I can also click back to close the modal as well and return to the previous page(state). I don't need help with Close at this point as I am still struggling with actually getting the modal working.


This is my code as it stands:

Navigation Controller: (is this even the correct place to put the modal functions?)

angular.module('cbuiRouterApp')
  .controller('NavbarCtrl', function ($scope, $location, Auth, $modal) {
    $scope.menu = [{
      'title': 'Home',
      'link': '/'
    }];

    $scope.open = function(){

        // open modal whithout changing url
        $modal.open({
          templateUrl: 'components/new-item/new-item.html'
        });

        // I need to open popup via $state.go or something like this
        $scope.close = function(result){
          $modal.close(result);
        };
      };

    $scope.isCollapsed = true;
    $scope.isLoggedIn = Auth.isLoggedIn;
    $scope.isAdmin = Auth.isAdmin;
    $scope.getCurrentUser = Auth.getCurrentUser;

    $scope.logout = function() {
      Auth.logout();
      $location.path('/login');
    };

    $scope.isActive = function(route) {
      return route === $location.path();
    };
  });

This is how I am activating the modal:

 <li ng-show='isLoggedIn()' ng-class='{active: isActive("/new-item")}'>
   <a href='javascript: void 0;' ng-click='open()'>New Item</a>
 </li>

new-item.html:

<div class="modal-header">
  <h3 class="modal-title">I'm a modal!</h3>
</div>
<div class="modal-body">
  <ul>
    <li ng-repeat="item in items"><a ng-click="selected.item = item">{{ item }}</a></li>
  </ul>Selected:<b>{{ selected.item }}</b>
</div>
<div class="modal-footer">
  <button ng-click="ok()" class="btn btn-primary">OK</button>
  <button ng-click="close()" class="btn btn-primary">OK</button>
</div>

Also whilst this does open a modal it doesn't close it as I couldn't work that out.

Prashant Pokhriyal
  • 3,727
  • 4
  • 28
  • 40
Daimz
  • 3,243
  • 14
  • 49
  • 76
  • Are you able to close your modal window when clicking close? I guess no. right? – micronyks Jul 12 '14 at 13:38
  • No, I can't get it to close. I can only open it and even then not only does it not close but it isn't functioning with the state and url Change so things like active links won't work. – Daimz Jul 12 '14 at 13:46
  • I have made a Plunk with my latest attempt to solve this but it''s not working. http://plnkr.co/edit/k514Nc25zfr0amtnxXDu?p=preview – Daimz Jul 12 '14 at 14:24
  • I just saw your plunker I've to say I didnt get anything from that , its totaly wrong, You've defined a controller inside your open-modal function Brother it's better you explain your actual problem first,Believe me , a good question worth 100 bad answers – Milad Jul 12 '14 at 15:05

4 Answers4

56

It's intuitive to think of a modal as the view component of a state. Take a state definition with a view template, a controller and maybe some resolves. Each of those features also applies to the definition of a modal. Go a step further and link state entry to opening the modal and state exit to closing the modal, and if you can encapsulate all of the plumbing then you have a mechanism that can be used just like a state with ui-sref or $state.go for entry and the back button or more modal-specific triggers for exit.

I've studied this fairly extensively, and my approach was to create a modal state provider that could be used analogously to $stateProvider when configuring a module to define states that were bound to modals. At the time, I was specifically interested in unifying control over modal dismissal through state and modal events which gets more complicated than what you're asking for, so here is a simplified example.

The key is making the modal the responsibility of the state and using hooks that modal provides to keep the state in sync with independent interactions that modal supports through the scope or its UI.

.provider('modalState', function($stateProvider) {
    var provider = this;
    this.$get = function() {
        return provider;
    }
    this.state = function(stateName, options) {
        var modalInstance;
        $stateProvider.state(stateName, {
            url: options.url,
            onEnter: function($modal, $state) {
                modalInstance = $modal.open(options);
                modalInstance.result['finally'](function() {
                    modalInstance = null;
                    if ($state.$current.name === stateName) {
                        $state.go('^');
                    }
                });
            },
            onExit: function() {
                if (modalInstance) {
                    modalInstance.close();
                }
            }
        });
    };
})

State entry launches the modal. State exit closes it. The modal might close on its own (ex: via backdrop click), so you have to observe that and update the state.

The benefit of this approach is that your app continues to interact mainly with states and state-related concepts. If you later decide to turn the modal into a conventional view or vice-versa, then very little code needs to change.

Nathan Williams
  • 951
  • 9
  • 12
  • That was sooooo helpful! Thanks so much, and it all makes sense too which is even better. I did run into one more problem tho. This works if I am on state `main` but if i go to another state `about` I get this `Could not resolve '.add' from state 'about'` I need a way to allow this to work on top of any state About, Main, Contact etc as it is accessible from the main navigation. – Daimz Jul 15 '14 at 11:48
  • The dot prefix is for relative navigation to a child state. If add is a sibling of about, then you would need an expression like ```^.add``` to get to add from about. Take a look at the documentation for [$state.go](http://angular-ui.github.io/ui-router/site/#/api/ui.router.state.$state#go) and see if that helps. – Nathan Williams Jul 15 '14 at 14:21
  • I had a read through, but I still can't get it working. I forked your plnkr and made a few changes to illustrate what I am doing. http://plnkr.co/edit/eupjB1i0djFGLcaGCD8j?p=preview Perhaps I am putting the '^' in the wrong place but I did also try put it in `$state.go('^.add')` and that didn't work either. – Daimz Jul 15 '14 at 21:52
  • 1
    Relative expressions are used for navigation (```$state.go```). When declaring a state, you either have to use the fully-qualified name or reference a parent state. So ```^.modal1``` is not valid when declaring a state. Here's a [working example](http://plnkr.co/edit/ip2NWMrlpePQ3sjfFL7T?p=preview). Every state has to exist on its own. You can reuse the templates and controllers, but there has to be a state declaration for ```main.modal1``` and ```about.modal1```. – Nathan Williams Jul 16 '14 at 14:14
  • Oh ok, I get what your saying and I suppose it makes sense. I was hoping there was a sneaky way I could just delare it once and have it just work no matter the state, but having to do it this way will be fine as well. Thanks for all the help – Daimz Jul 18 '14 at 06:04
  • 1
    You can use the ```$stateNotFound``` event to define states on the fly. Each state must have its own definition, but you can reuse templates and controllers. The hierarchical nature of state definitions ensures that the names and urls are unique even if the content is the same. [This example](http://plnkr.co/edit/RVGRrxnbezxGb4m4kAvK?p=preview) shows the basic idea. (Note that due to the way it's setup, it wouldn't work for bootstrap from a deep/direct link to modal2, but that's something you can address per the needs of your own implementation.) – Nathan Williams Jul 18 '14 at 16:24
  • Very nice but the only challenge with this is you can't use other state options such as `resolve`, `onEnter` and others because they are not called – Emeka Mbah Aug 21 '15 at 10:03
  • Where I've used this pattern in my own code, I tend to overload the options object with properties for both the state and the modal. Then I extract and incorporate the state-specific options in the call to the state provider. For hooks like `onEnter` and `onExit` that means some extra code to call the client functions passed as options. It's all straightforward enough, but not really necessary for this example. – Nathan Williams Aug 22 '15 at 14:19
  • Just include the `resolve` property in the options you pass in when setting up the state. The options object gets handed to `$modal.open` which will take it from there. If your resolves are not known until the actual state transition, then it's up to you to wait until they're settled to trigger it. – Nathan Williams Sep 07 '15 at 12:42
  • I solved the problem basically the same way. The only difference was that I placed the dialog opening/closing logic directly in the state's definition in the router, rather than abstracting it out and adding a `modalState` method to the $stateProvider. – Sean the Bean Jul 21 '16 at 16:01
  • It's worth noting that the service is now called `$uibModal`, not `$modal` (as of angular-ui/bootstrap v0.14.0), and that if you have strict DI enabled, the `onEnter` definition would look like `onEnter: ['$uibModal', '$state', function($modal, $state) {...}]` – Sean the Bean Jul 21 '16 at 16:09
  • @NathanWilliams could you take a look at http://stackoverflow.com/questions/40767450/how-to-use-angular-ui-router-with-angular-bootstrap-modal-uibmodal and this https://plnkr.co/edit/HJT1f1C23s2HQ2jTcy9p?p=preview – Abhijit Mazumder Nov 25 '16 at 06:21
8

Here is a provider that improves @nathan-williams solution by passing resolve section down to the controller:

.provider('modalState', ['$stateProvider', function($stateProvider) {
  var provider = this;

  this.$get = function() {
    return provider;
  }

  this.state = function(stateName, options) {
    var modalInstance;

    options.onEnter = onEnter;
    options.onExit = onExit;
    if (!options.resolve) options.resolve = [];

    var resolveKeys = angular.isArray(options.resolve) ? options.resolve : Object.keys(options.resolve);
    $stateProvider.state(stateName, omit(options, ['template', 'templateUrl', 'controller', 'controllerAs']));

    onEnter.$inject = ['$uibModal', '$state', '$timeout'].concat(resolveKeys);
    function onEnter($modal, $state, $timeout) {
      options.resolve = {};

      for (var i = onEnter.$inject.length - resolveKeys.length; i < onEnter.$inject.length; i++) {
        (function(key, val) {
          options.resolve[key] = function() { return val }
        })(onEnter.$inject[i], arguments[i]);
      }

      $timeout(function() { // to let populate $stateParams
        modalInstance = $modal.open(options);
        modalInstance.result.finally(function() {
          $timeout(function() { // to let populate $state.$current
            if ($state.$current.name === stateName)
              $state.go(options.parent || '^');
          });
        });
      });
    }

    function onExit() {
      if (modalInstance)
        modalInstance.close();
    }

    return provider;
  }
}]);

function omit(object, forbidenKeys) {
  var prunedObject = {};
  for (var key in object)
    if (forbidenKeys.indexOf(key) === -1)
      prunedObject[key] = object[key];
  return prunedObject;
}

then use it like that:

.config(['modalStateProvider', function(modalStateProvider) {
  modalStateProvider
    .state('...', {
      url: '...',
      templateUrl: '...',
      controller: '...',
      resolve: {
        ...
      }
    })
}]);
Dawid Grzesiak
  • 402
  • 4
  • 9
  • Could you take a look at http://stackoverflow.com/questions/40767450/how-to-use-angular-ui-router-with-angular-bootstrap-modal-uibmodal please – Abhijit Mazumder Nov 25 '16 at 06:22
  • From UI-Bootstrap 1.3.3, It is not necessary any more ! "$resolve" is available in template and his members can be injected in controller :) – TeChn4K Dec 08 '16 at 16:02
2

I answered a similar question, and provided an example here:

Modal window with custom URL in AngularJS

Has a complete working HTML and a link to plunker.

Community
  • 1
  • 1
cornernote
  • 1,055
  • 1
  • 12
  • 20
0

The $modal itself doesn't have a close() funcftion , I mean If you console.log($modal) , You can see that there is just an open() function.

Closing the modal relies on $modalInstance object , that you can use in your modalController.

So This : $modal.close(result) is not actually a function!

Notice : console.log($modal); ==>> result :

          Object { open: a.$get</k.open() }
           // see ? just open ! , no close !

There is some way to solve this , one way is :

First you must define a controller in your modal like this :

   $modal.open({
      templateUrl: 'components/new-item/new-item.html',
      controller:"MyModalController"
    });

And then , Later on , :

    app.controller('MyModalController',function($scope,$modalInstance){
      $scope.closeMyModal = function(){
       $modalInstance.close(result);
        }
       // Notice that, This $scope is a seperate scope from your NavbarCtrl,
       // If you want to have that scope here you must resolve it

   });
Milad
  • 27,506
  • 11
  • 76
  • 85
  • I meantioned above that I tried a new method to get this working as shown in this plunk http://plnkr.co/edit/k514Nc25zfr0amtnxXDu?p=preview but the reason I bring this up it that method relies upon resolve as well. I had a look to try see what resolve was actually for but it didn't make sense, would you mind elaborating on how it works and why I have to resolve it to have the NavbarCtrl in there? – Daimz Jul 12 '14 at 14:27
  • I still dont get your problem :( 1- do you want to know what is the resolve and how it works ? 2- closeing modal is your problem ? – Milad Jul 12 '14 at 14:56
  • @xe4me, This is right. there are also second method that you can use if you don't want to use $modalInstance. second way is, var myModal=$modal.open({ templateUrl: 'myHtml.html'}); myModal.close(); <-----this method would also work when you click close button. – micronyks Jul 12 '14 at 14:58
  • @Daimz. You need to be more specific when you ask any question. Bcos there are many ways to solve particular problem. But firstly you need to be specific what you what n how? We don't get your problem identified. so it becomes problematic for us to help you. – micronyks Jul 12 '14 at 15:02
  • @micronyks Yes , ofcourse , I wanted to write that too but I remembered in the official angular-ui-bootstrap they've explained it well , So I thought he maybe have seen that approach already ! But thanks – Milad Jul 12 '14 at 15:02
  • @micronyks Fair point, I thought I had made it clear in my original post. Closing the form is simply something I mentioned I couldn't do, not the the main problem I needed help with. My problem is I want a link in my navigation 'new item' that when clicked uses Ui-routers 'states' to load new-item.html as a modal. I would like the state to have actually changed so /home becomes /new-item when the link is clicked and the modal loads that way I can click on the browsers 'back' button to close and return to /home closing the modal. I have tried and tried but I am lost as how to do this. – Daimz Jul 13 '14 at 12:22
  • I have updated my Plnkr to try give a better understanding, http://plnkr.co/edit/k514Nc25zfr0amtnxXDu?p=preview – Daimz Jul 13 '14 at 13:53