Reputation: 14521
I have a abstract profile
state, which has multiple child states (for different tabs of the profile page), and then I want to have another child state of profile be a modal. I've implemented it something like this:
$stateProvider
.state('profile', {
url: '/profile/:profileID',
templateUrl: 'profile.html',
controller: 'ProfileCtrl',
abstract:true,
resolve: {
user: ...
}
})
.state('profile.circles', {
url: '',
templateUrl: 'profilecircles.html',
controller: 'profilecirclesCtrl',
resolve: { circle: ... }
})
.state('profile.squares', {
url: '/collections',
templateUrl: 'profilesquares.html',
controller: 'profilesquaresCtrl',
resolve: { squares: ... }
})
.state('profile.editprofile', {
url: '/edit',
onEnter: ['$window','$modal', function($window, $modal) {
$modal.open({
templateUrl: 'editprofile.html',
controller: 'editProfileCtrl',
resolve: {
user: ...
}
}).result.then(function() {
$window.history.back();
},function() {
$window.history.back();
});
}]
})
This works great, except for the fact that because editprofile is a sibling of squares and circles, when that state is active and the modal is in view, the squares or circle state is unloaded, and loaded back in again when the modal is closed.
Is there any way to have those states remain active when the profile.editprofile state is active? I'm after something like state..editprofile
.
Upvotes: 8
Views: 3400
Reputation: 18034
You can designate your tab states sticky: true
by using ui-router-extras. Sticky states are preserved when navigating to a sibling and are especially useful for tabs.
To use it, you'll have to create one named view for each sticky state (you can hide inactive ones however, see below).
The API page describes the necessary steps:
sticky: true
views: { bar: { /* bar named-view definition */ } }
<div ui-view="bar" />
<div ui-view="bar" ng-show="$state.includes("foo.bar") />
Upvotes: 0
Reputation: 43785
As there is currently no ideal solution, I have come up with a reasonably elegant solution. My code is generalized, easy to understand and commented so that it should be easy to adapt to any project.
See the comments in the code for the most significant points.
$stateProvider
// modal will open over this state from any substate
.state('foo', {
abstract: true,
url: '/:fooProp',
templateUrl: 'foo.html',
controller: 'FooController'
})
.state('foo.main', {
url: '',
templateUrl: 'foo-main.html',
controller: 'FooMainController'
})
.state('foo.bar', {
url: '/bars/:barId',
templateUrl: 'foo-bar.html',
controller: 'FooBarController'
})
// append /modal route to each `foo` substate
.state('foo.main.modal', getModalState())
.state('foo.bar.modal', getModalState())
;
function getModalState() {
return {
url: '/modal',
onEnter: [
'$modal',
'$state',
function($modal, $state) {
$modal.open({
templateUrl: 'foo-modal.html',
controller: 'FooModalController'
}).result.finally(function() {
// go to parent state - the current `foo` substate
$state.go('^');
});
}
]
}
}
$scope.goToModalState = function() {
// go to this state's `modal` state
$state.go($state.current.name+'.modal');
};
<a ng-click="goToModalState()">Open Modal</a>
Upvotes: 3
Reputation: 6269
The point of ui-router is that the states nesting matches the url nesting. What that means is that in your case if you have:
profile.circles with a url: /profile/123
profiles.squares with a url: /profile/123/collections
profiles.edit with a url: /profile/123/edit
You can't expect to have profile.squares and profiles.edit active at the same time since they are two completely different urls. However It is possible to have:
profiles.circles.editprofile with a url: /profile/123/edit
That will keep profiles.circles active during your modal since it's a parent state. But in the case of profiles.squares, if you want to have a state representing editprofile you would need something like:
profiles.squares.editprofile with a url: /profile/123/collection/edit
Unfortunately there is no other way around it if you want to keep edit as a state.
Upvotes: 1
Reputation: 4911
Well unless you want ditch the modal implementation I would suggest then having 2 views in profile.html
one for editprofile.html
and other for profilecircles.html
or profilesquares.html
, something like following:
// profile.html
...
<div class="" ui-view="edit" autoscroll="false"></div>
<div class="" ui-view="content" autoscroll="false"></div>
...
// state config
$stateProvider
.state('profile', {
url: '/profile/:profileID',
templateUrl: 'profile.html',
controller: 'ProfileCtrl',
abstract:true,
resolve: {
user: ...
}
})
.state('profile.circles', {
url: '',
views: {
edit@: {
templateUrl: 'editprofile.html',
controller: 'editProfileCtrl'
},
content@: {
templateUrl: 'profilecircles.html',
controller: 'profilecirclesCtrl'
}
}
resolve: { circle: ... }
})
.state('profile.squares', {
url: '/collections',
views: {
edit@: {
templateUrl: 'editprofile.html',
controller: 'editProfileCtrl'
},
content@: {
templateUrl: 'profilesquares.html',
controller: 'profilesquaresCtrl'
}
}
resolve: { squares: ... }
})
And then the hard part for actually showing/hiding the edit profile view. But that can be done with some toggle button or after action is validated in controller.
Upvotes: 2