I have a service which checks whether the current user has access to the route.
I would like that service to be called as part of the resolve, so that the view never loads if the user should not have access. However, within the resolve function the $state dependency does not yet contain the actual state of the router.
.state('home', {
url: '/home',
templateUrl: 'app/home.html',
controller: 'HomeController',
controllerAs: 'main',
resolve: {
allowed: function(auth, $state) {
return auth.isAllowed($state.current.name);
}
},
})
... however $state.current.name is empty at the point which it is used. How can I pass the state's name (i.e. "home")?
Try with
resolve: {
allowed: function(auth) {
return auth.isAllowed(this.self.name);
}
},
For UI router 1.0+: inject $state$ to get access to future state (to which transition is in progress). However, if resolve is in parent or abstract state, it will point to that abstract state, not child state to which transition is.
resolve: {
allowed: function($state$) {
...
}
}
Related
I am trying to set up some states as follows
state('search', {
url: '',
templateUrl: '/modules/core/views/search.html',
controller: 'SearchController'
}).
state('check', {
controller: 'CheckController',
abstract: true,
template: '<ui-view></ui-view>',
params: {
subject: null
}
}).
state('check.status', {
templateUrl: '/modules/core/views/check-status.client.view.html'
}).
state('check.personal', {
templateUrl: '/modules/core/views/check-personal.client.view.html'
});
Search results are passed into the 'check' state with parameterised ui-sref's.
Within each child state of 'check' I want to change some properties on subject and then call $state.go (preferably without passing subject as a parameter) to move along to the next state in the journey.
I'm finding though that
CheckController is reloaded if the parameter is not supplied on $state.go to subsequent states or if I supply it but change it slightly.
If CheckController is reloaded, changes made to the subject object aren't persisted through the transition as if $stateParams contains a copy of the subject object taken at some point earlier by ui-router.
Is there a way to prevent / control either of these behaviours?
Edit:
I found this pattern works out:
state('check.status', {
templateUrl: '/modules/core/views/check-status.client.view.html',
params: {
subject: null
},
controller: function($scope, $stateParams) {
$scope.$parent.subject = $stateParams.subject;
}
}).
Removing the param from the abstract parent, adding it to the first state in the journey and assigning the property on the parent scope with a small controller.
I think in your config for your child states you can tell ui-router to prevent reloading with this:
state('check.status', {
templateUrl: '/modules/core/views/check-status.client.view.html',
reloadOnSearch: false
})
I am trying to create a parent-child relationship in my UI router config with using different resolves and it doesn't seem to be working. I want to have a common parent state with child states that control whether the state is in edit mode or new mode. Depending on the mode, the resolve of the state is different.
What I have is essentially this:
.state('main.details', {
url: "/details",
templateUrl: "modules/details.html",
abstract: true
})
.state('main.details.new', {
controller: "DetailsCtrl as detailsCtrl",
resolve : {
detail: ['$stateParams', 'NewService', function ($stateParams, NewService) {
return NewService.getDetail($stateParams.detailId, true);
}]
}
})
.state('main.details.edit', {
controller: "DetailsCtrl as detailsCtrl",
resolve : {
detail: ['$stateParams', 'EditService', function ($stateParams, EditService) {
return EditService.getDetail($stateParams.detailId, true);
}]
}
})
Doing it this way, I get this:
Error: [$injector:unpr] Unknown provider: detailProvider <- detail <- DetailsCtrl
Is there a way to have a parent state that just defines the URL and templateUrl and then have child states with varying resolves?
One option to help with this would be to pull out the repeating resolve on the routes and put it into a factory that the controller can call on
I'm making a single page application (SPA). I made a controller called InitialControler to load the data from the server at this url (local.app/init).
I want this url to be opened before any other url. I'm using ui-router, I did a $state.go('init') in the .run() function but it still load the requested page before the 'init' page
First create state called app
$stateProvider.state('app', {
abstract: true,
templateUrl: "assets/partials/container.html",
controller: 'AppCtrl',
resolve: {
init: function(MyFactory) {
return MyFactory.resolver();
}
}
});
Now, any new state you create should be child state of app state. This is also good because it become sort of your root scope. And state will not process unless your factory resolves.
This is how you create your factory
app.factory('MyFactory', function($http){
var items = [];
return {
resolver: function(){
return $http.get('my/api').success(function(data){
items = data;
})
},
get() {
return items;
}
}
});
Now in any other state
$stateProvider.state('app.items', {
url: '/items',
templateUrl: "assets/partials/items.html",
controller: function($scope, MyFactory){
$scope.items = MyFactory.get();
}
});
More on sate resolve
https://github.com/angular-ui/ui-router/wiki#resolve
If you are using ui-router then you could resolve this using nested states. For example:
$stateProvider
.state("main", {
url: "/",
template: '<div ui-view></div>',
controller: 'InitController'
})
.state("main.landing", {
url: "landing",
templateUrl: "modules/home/views/landing.html",
controller: 'LandingPageController'
})
.state("main.profile", {
url: "profile",
templateUrl: "modules/home/views/profile.html",
controller: 'ProfileController'
});
In this example you have defined 3 routes: "/", "/landing", "/profile"
So, InitController (related to "/" route) gets called always, even if the user enters directly at /landing or /profile
Important: Don't forget to include <div ui-view></div> to enable the child states controller load on this section
One way to do is, in config declare only 'init' state. And in InitialController, after data is loaded(resolve function of service call), configure other states. But in this approach, whenever you refresh the page, the url will change to local.app.init.
To stay in that particular state even after reloading, the solution I found is to have a StartUp app in which I loaded the required data and after that I bootstraped the main app manually by angular.bootstrap.
States are set up to allow for re-use by injecting parent resolve entries into children, which all works except for the creation of a new package instance. I can't figure out how to determine that the dashboard.package state is the actual state that is be transitioned to. Even if the second parameter meant for the child state is present only one appears in $state.params so I can't check for isUndefined and know that dashboard.package is the state.
Logic for the states are if no second parameter is present no document exists and new instance needs to be created, otherwise state is edit and instance exists.
// Parent dashboard
.state('dashboard', {
url: "/dashboard",
abstract: true,
templateUrl: '/app/dashboard.html',
resolve: {
UserAuth: ...,
GetPackageTypes: ...
}
}
// Parent dashboard package
.state('dashboard.package', {
url: "/package/:packageInstance",
templateUrl: '/app/dashboard/views/package.html',
controller: 'PackageController',
controllerAs: 'packageCtrl',
resolve: {
GetPackageType: [function(){
// HTTP request for package type
}],
CreatePackage: ['$state', '$stateParams', 'GetPackageType',
function($state, $stateParams, GetPackageType){
// ISSUE: Determine if dashboard.package is the actual state???
// if it is then create new package instance, otherwise will drop
// into dashboard.package.edit and package instance will be used
// Example URL for this state: #/dashboard/package/type
// Even if both params exist only see one here so no good
console.log("state.params = ", $state.params);
// Shows previous state name so no good
console.log("state.current = ", $state.current);
console.log("state.current.name = ", $state.current.name);
console.log("state.$current.self.name = ", $state.$current.self.name);
// Returns false so no good
console.log("$state.is = ", $state.is('dashboard.package') );
console.log("$state includes = ", $state.includes('dashboard.package') );
}]
}
}
// Child dashboard package
.state('dashboard.package.edit', {
url: "/edit/:packageInstanceId",
templateUrl: '/app/dashboard/views/package.html',
controller: 'PackageController',
controllerAs: 'packageCtrl',
resolve: {
GetPackageInstance: ['$state', '$stateParams', 'GetPackageType',
function($state, $stateParams, GetPackageType){
// HTTP request for package instance uses package type
// Example URL for this state: #/dashboard/package/type/edit/3858
}],
}
}
ALTERNATIVE SOLUTION
To avoid any more wasted time trying to figure this out, or putting together some funky work around, which will potentially fail in some future update I ended up creating a second abstract state dashboard.package with all my controller, templateUrl, and initial resolve with a single parameter for the package type, and splitting create package out into separate state dashboard.package.create at same level as dashboard.package.edit. Works great with no headache if any finds it useful.
// Parent dashboard
.state('dashboard', {
url: "/dashboard",
abstract: true,
templateUrl: '/app/dashboard.html',
resolve: {
UserAuth: ...,
GetPackageTypes: ...
}
}
.state('dashboard.package', {
url: "/package/:packageType",
abstract: true,
templateUrl: '/app/dashboard/views/package.html',
controller: 'PackageController',
controllerAs: 'packageCtrl',
resolve: {
GetPackage: // injecting parent GetPackageTypes and using parameter
}
}
.state('dashboard.package.create', {
url: "",
resolve: {
CreatePackage: // injecting parent GetPackage
}
}
.state('dashboard.package.edit', {
url: "/edit/:packageinstance",
resolve: {
GetPackageInstance: // injecting parent GetPackage and using parameter
}
}
//$state.current.name will have either name of parent or child.
resolve: {
isChild: function($state, STATES) {
return $state.current.name === STATES.CHILD_NAME;
}
}
Is it possible to setup a route in ui-router that only has a controller? The purpose being that at a certain URL, the only thing I'd like to do is take action programatically, and not display anything in terms of a view. I've read through the docs, but I'm not sure if they offer a way to do this.
Yes, I have read this: https://github.com/angular-ui/ui-router/wiki/Frequently-Asked-Questions#how-to-open-a-dialogmodal-at-a-certain-state, but that is not quite what I am looking for.
For example, let's just say I have a basic body with view:
<body ui-view></body>
And some basic config:
// Routes
$stateProvider
.state('myaction', {
url: "/go/myaction",
onEnter: function() {
console.log('doing something');
}
});
When /go/myaction is visited, the view is blank. Is it possible to do this?
I was able to solve this problem by redirecting the headless state I was taking programmatic action in, to a state WITH a view at the end of the headless state:
$stateProvider
.state('myaction', {
url: "/go/myaction",
onEnter: function() {
console.log('doing something');
}
controller: function($state) {
$state.go('home');
}
});
You can't have a controller without a view but you can use onEnter instead of a controller. If you don't want to change the current view when accessing this state you can define it as a child state:
$stateProvider
// the parent state with a template
.state('home', {
url: '/home',
templateUrl: '/home.html',
controller: 'HomeCtrl'
})
// child of the 'home' state with no view
.state('home.action', {
url: '/action',
onEnter: function() {
alert('Hi');
},
});
Now in home.html you can do something like this:
<a href ui-sref=".action">Greet me!</a>
From the docs:
Warning: The controller will not be instantiated if template is not defined.
Why don't you use an empty string as a template to overcome this?
Yes, you can do that. Use absolute view names to re-use the <ui-view> of another state.
Take a look at this example:
Users go to my app, but depending on them being authenticated or not, I want to send them to a public or private page. I use the index state purely to see if they're logged in or not, and then redirect them to index.private or index.public.
The child states make use of absolute view names to use the <ui-view> element that corresponds to the index state. This way, I don't need to make a second nested <ui-view>.
$stateProvider.state('index', {
url: "/",
controller: 'IndexCtrl'
}).state('index.private', {
views: {
"#": {
templateUrl: 'private.html',
controller: 'PrivateCtrl'
}
}
}).state('index.public', {
views: {
"#": {
templateUrl: 'public.html',
controller: 'PublicCtrl'
}
}
});
A small note on this example: I'm using the # shortcut here. Normally you would use viewname#statename.
My solution for this was just to include a template (html file) that is blank.