Using ui-router with Bootstrap-ui modal
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.
Daimz
Im a Graphic Designer likes experimenting with HTML5, CSS3, and JQuery
Updated on July 25, 2020Comments
-
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 almost 10 yearsI 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 almost 10 yearsI 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 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 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 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 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 almost 10 yearsI have updated my Plnkr to try give a better understanding, plnkr.co/edit/k514Nc25zfr0amtnxXDu?p=preview
-
Daimz almost 10 yearsThat 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 stateabout
I get thisCould 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 almost 10 yearsThe 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 almost 10 yearsI 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 almost 10 yearsRelative 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 formain.modal1
andabout.modal1
. -
Daimz almost 10 yearsOh 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 almost 10 yearsYou 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 almost 9 yearsVery 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 almost 9 yearsWhere 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
andonExit
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 almost 9 yearsJust 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 almost 8 yearsI 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 almost 8 yearsIt'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, theonEnter
definition would look likeonEnter: ['$uibModal', '$state', function($modal, $state) {...}]
-
Abhijit Mazumder over 7 years@NathanWilliams could you take a look at stackoverflow.com/questions/40767450/… and this plnkr.co/edit/HJT1f1C23s2HQ2jTcy9p?p=preview
-
Abhijit Mazumder over 7 yearsCould you take a look at stackoverflow.com/questions/40767450/… please
-
TeChn4K over 7 yearsFrom UI-Bootstrap 1.3.3, It is not necessary any more ! "$resolve" is available in template and his members can be injected in controller :)