I need to display a modal window (basically just a hidden div that will be loaded with a compiled template) in my Angular app. The problem is, I need the URL to change when the modal opens so users can copy the link and go directly to the modal window and also use the back button to close the modal window and return to the previous page. This is similar to the way Pinterest handles modal windows when you click on a pin.
So far I've created a directive that loads the template, compiles it using $compile, injects the $scope and then displays the compiled template. This works fine.
The problem is as soon as I use $location to change the path, the route controller fires and loads the template into ng-view.
I thought of 2 ways of overcoming this, but have not been able to implement either:
Somehow prevent the route controller from firing when I change the url using $location. I've added a listener to $routeChangeStart to prevent the default from happening, but that does not seem to work.
Somehow add another view handler to the page (basically have 2 named ng-view directives on the page) and have each able to handle different routes. Can't see that Angular supports this at the moment though.
The URL needs to be of the format /item/item_id and not /item?item_id=12345.
Any help would be appreciated.
You can now do this if you use the ui.router module as described here: https://github.com/angular-ui/ui-router/wiki/Frequently-Asked-Questions#how-to-open-a-dialogmodal-at-a-certain-state.
Their example:
$stateProvider.state("items.add", {
url: "/add",
onEnter: function($stateParams, $state, $modal, $resource) {
$modal.open({
templateUrl: "items/add",
resolve: {
item: function() { new Item(123).get(); }
},
controller: ['$scope', 'item', function($scope, item) {
$scope.dismiss = function() {
$scope.$dismiss();
};
$scope.save = function() {
item.update().then(function() {
$scope.$close(true);
});
};
}]
}).result.then(function(result) {
if (result) {
return $state.transitionTo("items");
}
});
}
});
I also pass an 'errorCallback' function to the then() function to handle modal dismissal by pressing Escape or clicking on the background.
I had similar requirements. I needed:
I couldn't find a good example of all of this working together, but I was able to find examples of bits and pieces, so I put them all together into a working example as follows:
<!DOCTYPE html>
<html lang="en" ng-app="app">
<head>
<meta charset="UTF-8">
<script src="//cdnjs.cloudflare.com/ajax/libs/angular.js/1.3.0/angular.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/angular-ui-router/0.2.11/angular-ui-router.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/angular-ui-bootstrap/0.11.2/ui-bootstrap-tpls.js"></script>
<link href="//cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.1/css/bootstrap.css" rel="stylesheet">
<script>
// init the app with ui.router and ui.bootstrap modules
var app = angular.module('app', ['ui.router', 'ui.bootstrap']);
// make back button handle closing the modal
app.run(['$rootScope', '$modalStack',
function($rootScope, $modalStack) {
$rootScope.$on('$stateChangeStart', function() {
var top = $modalStack.getTop();
if (top) {
$modalStack.dismiss(top.key);
}
});
}
]);
// configure the stateProvider
app.config(['$stateProvider', '$urlRouterProvider',
function($stateProvider, $urlRouterProvider) {
// default page to be "/" (home)
$urlRouterProvider.otherwise('/');
// configure the route states
$stateProvider
// define home route "/"
.state('home', {
url: '/'
})
// define modal route "/modal"
.state('modal', {
url: '/modal',
// trigger the modal to open when this route is active
onEnter: ['$stateParams', '$state', '$modal',
function($stateParams, $state, $modal) {
$modal
// handle modal open
.open({
template: '<div class="modal-header"><h3 class="modal-title">Modal</h3></div><div class="modal-body">The modal body...</div><div class="modal-footer"><button class="btn btn-primary" ng-click="ok()">OK</button><button class="btn btn-warning" ng-click="cancel()">Cancel</button></div>',
controller: ['$scope',
function($scope) {
// handle after clicking Cancel button
$scope.cancel = function() {
$scope.$dismiss();
};
// close modal after clicking OK button
$scope.ok = function() {
$scope.$close(true);
};
}
]
})
// change route after modal result
.result.then(function() {
// change route after clicking OK button
$state.transitionTo('home');
}, function() {
// change route after clicking Cancel button or clicking background
$state.transitionTo('home');
});
}
]
});
}
]);
</script>
</head>
<body>
<a href="#/modal" class="btn btn-default">POPUP!</a>
<div ui-view></div>
</body>
</html>
You can view it in action at plunker: http://plnkr.co/edit/tLyfRP6sx9C9Vee7pOfC
Click the "Open the embedded view" link in plunker to see the hash tag working.
hermanschutte's solution worked (see comment on Tiago Roldão's post). I'm posting step by step to help clarify.
First, in the config:
.when('/posts', {
templateUrl: 'views/main.html',
controller: 'MainCtrl',
reloadOnSearch: false // this allows us to use the back button to get out of modals.
})
Then, on opening modal (you'll need the location module):
$location.search('modal');
And in modal controller:
$scope.$on('$locationChangeSuccess', function() {
$scope.close(); // call your close function
});
The named view solution you mentioned has had great discussion on the angularJS mailing list, but as of now (and the near to mid-future) the $route system doesn't handle multiple views.
As I have said in a few places, and here in SO a few times as well, the $route system is one of the situations where angular's very opinionated stance can become a hinderance: it's meant to be simple, but it isn´t very configurable. I personally don't use it directly, but rather bypass it via a solution I found here.
It complicates things a little, but it gives you greater flexibility - the idea being that the $route only serves to trigger a (manual) render function, where you can assign the values and trigger $broadcasts as you wish (normally, from the main controller).
One thing I haven't tried is a "hybrid" solution - that is, using the $route system normally, and configuring your /item/item_id route to NOT have a view parameter (so as to not change the main view, initiated by the other routes) and do something via the $routeChangeStart event (remember you can assign any value to the routes, as you can see by the page I referenced)
Again, personally I prefer fully using the alternative method.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With