Using ui-router with Bootstrap-ui modal

32,494

Solution 1

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.

Solution 2

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: {
        ...
      }
    })
}]);

Solution 3

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.

Share:
32,494
Daimz
Author by

Daimz

Im a Graphic Designer likes experimenting with HTML5, CSS3, and JQuery

Updated on July 25, 2020

Comments

  • Daimz
    Daimz almost 4 years

    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.

  • Daimz
    Daimz almost 10 years
    I meantioned above that I tried a new method to get this working as shown in this plunk 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?
  • Milad
    Milad almost 10 years
    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 ?
  • micronyks
    micronyks almost 10 years
    @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
    micronyks almost 10 years
    @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.
  • Milad
    Milad almost 10 years
    @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
  • Daimz
    Daimz almost 10 years
    @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
    Daimz almost 10 years
    I have updated my Plnkr to try give a better understanding, plnkr.co/edit/k514Nc25zfr0amtnxXDu?p=preview
  • Daimz
    Daimz almost 10 years
    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.
  • Nathan Williams
    Nathan Williams almost 10 years
    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 and see if that helps.
  • Daimz
    Daimz almost 10 years
    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. 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.
  • Nathan Williams
    Nathan Williams almost 10 years
    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. 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.
  • Daimz
    Daimz almost 10 years
    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
  • Nathan Williams
    Nathan Williams almost 10 years
    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 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.)
  • Emeka Mbah
    Emeka Mbah almost 9 years
    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
  • Nathan Williams
    Nathan Williams almost 9 years
    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
    Nathan Williams almost 9 years
    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.
  • Sean the Bean
    Sean the Bean almost 8 years
    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
    Sean the Bean almost 8 years
    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) {...}]
  • Abhijit Mazumder
    Abhijit Mazumder over 7 years
  • Abhijit Mazumder
    Abhijit Mazumder over 7 years
    Could you take a look at stackoverflow.com/questions/40767450/… please
  • TeChn4K
    TeChn4K over 7 years
    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 :)