ui-router automatically redirects out of state - angularjs

I've got the following route:
.state('app.base.patient', {
url: '/store/:storeID/patient/:patientID',
template: template,
controller: patientCtrl,
controllerAs: 'vm',
resolve: {
patientObj: ['$stateParams', 'patientResource', function($stateParams, patientResource) {
if ($stateParams.patientID !== '') {
return patientResource.get($stateParams.patientID);
}
return null;
}]
}
});
I'm able to navigate to it through the app perfectly fine (using $state.go)
which gets me to (for example):
'/store/105/patient/8a7f7e785279bc1f015279be7cb40001'
But when I try to refresh the page on with the same url I get redirected to:
'/store/105/patient/'
Any ideas what might be causing this? I've been adding more and more logging but I can't see anything that looks obviously wrong
The "parent" view is the store select view:
$stateProvider
.state('app.base.store', {
url: '/store',
template: template,
controller: storeCtrl,
controllerAs: 'vm'
});

Your URLs indicate the states should be nested, but the state definitions do not reflect that. Your patient state should be nested in your store state. The patient state would change to something like this:
.state('app.base.store.patient', {
url: '/patient/:patientID',
template: template,
controller: patientCtrl,
controllerAs: 'vm',
resolve: {
patientObj: ['$stateParams', 'patientResource', function($stateParams, patientResource) {
if ($stateParams.patientID !== '') {
return patientResource.get($stateParams.patientID);
}
return null;
}]
}
});
Meanwhile, your store state would look like this:
$stateProvider
.state('app.base.store', {
url: '/store/:storeID',
template: template,
controller: storeCtrl,
controllerAs: 'vm'
});
Then, as long as your views are nested properly, you can navigate to the patient state from the store view like so:
ui-sref="app.base.store.patient({patientID: vm.patientID})"
Or in controller code like so:
$state.go("app.base.store.patient", { storeID: vm.storeID, patientID: vm.patientID });

The problem turned out to be a rogue
this.$state.go('app.base.patient', {storeID: value.id, patientID: null});
I originally added the explicit patientID: null as I thought it's needed - it's not and will cause the said problem during load.

Related

Angular ui router controlleras syntax not working

I an trying to develop an angular app using ui router, however I am stuck trying to get the controllerAs syntax working correctly.
my stateProvider looks like this
$stateProvider
.state('microsite', {
url: "/",
templateUrl: "microsite.tmpl.html",
abstract: true
})
.state('microsite.home', {
url: "",
templateUrl: "home.tmpl.html",
controller: 'MicrositeController as vm',
data: {
page_name: 'Introduction'
}
})
.state('microsite.features', {
url: "/features",
templateUrl: "features.tmpl.html",
controller: 'MicrositeController as vm',
data: {
page_name: 'Features'
}
})
.state('microsite.about', {
url: "/about",
templateUrl: "about.tmpl.html",
controller: 'MicrositeController as vm',
data: {
page_name: 'About'
}
});
As you can see I setup an abstract default view, and three pages.
I have also assigned a data object with a page_name for each page. This feeds into my controller
myapp.controller('MicrositeController', ['$state', function($state) {
var vm = this;
vm.page_name = $state.current.data.page_name;
vm.sidenav_locked_open = false;
vm.toggleSideNav = function() {
if ($mdMedia('gt-sm')) {
vm.sidenav_locked_open = !vm.sidenav_locked_open;
} else {
$mdSidenav('left').toggle();
}
}
}]);
and then delivers the name to the page via the vm.page_name variable.
However this is not happening. The variable never makes it to the page.
Also I have a vm.toggleSideNav function that is suppose to open and close the sidenav, but that never gets called.
the toolbar with the sidenav button is this
<md-toolbar layout="row" class="md-whiteframe-glow-z1 site-content-toolbar">
<div class="md-toolbar-tools docs-toolbar-tools" tabIndex="-1">
<md-button class="md-icon-button" ng-click="vm.toggleSideNav()" aria-label="Toggle Menu">
XXX
</md-button>
<h1>{{vm.page_name}}</h1>
</div>
</md-toolbar>
here is a plnkr example http://plnkr.co/edit/Na5zkF?p=preview
I am looking for someone to help me figure out the last piece on how to get the toggleSideNav function to get called when I click on the xxx button, and how to get the title to display in the toolbar.
From the Docs:
controller
(optional)
string
function
Controller fn that should be associated with newly related scope or the name of a registered controller if passed as a string. Optionally, the ControllerAs may be declared here.
controller: "MyRegisteredController"
controller:
"MyRegisteredController as fooCtrl"
controller: function($scope, MyService) {
$scope.data = MyService.getData(); }
— UI Router $stateProvider API Reference.
According to the Docs, your controller declaration is correct.
controller: 'MicrositeController as vm'
You need to look for your problem elsewhere.
UPDATE
Put the controller in the root state:
$stateProvider
.state('microsite', {
url: "/",
templateUrl: "microsite.tmpl.html",
//IMPORTANT == Put controller on root state
controller: 'MicrositeController as vm',
abstract: true
})
.state('microsite.home', {
url: "",
templateUrl: "home.tmpl.html",
̶c̶o̶n̶t̶r̶o̶l̶l̶e̶r̶:̶ ̶'̶M̶i̶c̶r̶o̶s̶i̶t̶e̶C̶o̶n̶t̶r̶o̶l̶l̶e̶r̶ ̶a̶s̶ ̶v̶m̶'̶,̶
data: {
page_name: 'Introduction'
}
})
.state('microsite.features', {
url: "/features",
templateUrl: "features.tmpl.html",
̶c̶o̶n̶t̶r̶o̶l̶l̶e̶r̶:̶ ̶'̶M̶i̶c̶r̶o̶s̶i̶t̶e̶C̶o̶n̶t̶r̶o̶l̶l̶e̶r̶ ̶a̶s̶ ̶v̶m̶'̶,̶
data: {
page_name: 'Features'
}
})
.state('microsite.about', {
url: "/about",
templateUrl: "about.tmpl.html",
̶c̶o̶n̶t̶r̶o̶l̶l̶e̶r̶:̶ ̶'̶M̶i̶c̶r̶o̶s̶i̶t̶e̶C̶o̶n̶t̶r̶o̶l̶l̶e̶r̶ ̶a̶s̶ ̶v̶m̶'̶,̶
data: {
page_name: 'About'
}
});
})
The DEMO on PLNKR
Try adding the option controllerAs: 'vm' to the state params instead defining the controller as in the controller option.
Try adding the option controllerAs: 'vm' to the state params instead defining the controller as in the controller option.
or, if I'm not mistaken, you can add
myapp.controller('MicrositeController as vm' ...

Access scope variable from the other controller within the nested state

How to access other state scope variable from another state ctrl?
If in 'session.index.detail' state, I want to access leftPanel scope variable from rightPanel Ctrl is it possible? What I want to do is I have a form in rightPanel, when I click save, I want to add the new data object back to leftPanel list and refresh it after it is successfully added to the database.
.state('session', {
abstract: true,
templateUrl: '/schedule/_session.html'
})
.state('session.index', {
url: '/sessionmang',
views: {
'leftPanel#session': {
controller: 'SessionCtrl',
controllerAs: 'vm',
templateUrl: '/schedule/_sessionPanel.html'
},
'rightPanel#session': {
templateUrl: '/schedule/_sessionDetailPanel.html'
}
}
})
.state('session.index.detail', {
params: {
sessionObj: null,
sessionTypeObj: null
},
views: {
'rightPanel#session': {
controller: 'SessionDetailCtrl',
controllerAs: 'vm',
templateUrl: '/schedule/_sessionDetailPanel.html',
resolve: {
sessionObj: ['$stateParams', function ($stateParams) {
return $stateParams.sessionObj;
}],
sessionTypeObj: ['$stateParams', function ($stateParams) {
return $stateParams.sessionTypeObj;
}]
}
}
}
});
I think the best way is to create a service that holds the data you want to share.
And share it between the controllers.
Or use broadcasting to talk between the controllers and pass data over that way
the simplest way is, you could use events like this.
when you click save, you will do
$scope.$broadcast('myevent', data);
from the rightCtrl and from the leftCtrl you could do
$scope.$on('myevent', function(event,data) {
//do your updates here
})

Angular ui-router to accomplish a conditional view

I am asking a similar question to this question: UI Router conditional ui views?, but my situation is a little more complex and I cannot seem to get the provided answer to work.
Basically, I have a url that can be rendered two very different ways, depending on the type of entity that the url points to.
Here is what I am currently trying
$stateProvider
.state('home', {
url : '/{id}',
resolve: {
entity: function($stateParams, RestService) {
return RestService.getEntity($stateParams.id);
}
},
template: 'Home Template <ui-view></ui-view>',
onEnter: function($state, entity) {
if (entity.Type == 'first') {
$state.transitionTo('home.first');
} else {
$state.transitionTo('home.second');
}
}
})
.state('home.first', {
url: '',
templateUrl: 'first.html',
controller: 'FirstController'
})
.state('home.second', {
url: '',
templateUrl: 'second.html',
controller: 'SecondController'
});
I set up a Resolve to fetch the actual entity from a restful service.
Every thing seems to be working until I actually get to the transitionTo based on the type.
The transition seems to work, except the resolve re-fires and the getEntity fails because the id is null.
I've tried to send the id to the transitionTo calls, but then it still tries to do a second resolve, meaning the entity is fetched from the rest service twice.
What seems to be happening is that in the onEnter handler, the state hasn't actually changed yet, so when the transition happens, it thinks it is transitioning to a whole new state rather than to a child state. This is further evidenced because when I remove the entity. from the state name in the transitionTo, it believes the current state is root, rather than home. This also prevents me from using 'go' instead of transitionTo.
Any ideas?
The templateUrl can be a function as well so you check the type and return a different view and define the controller in the view rather than as part of the state configuration. You cannot inject parameters to templateUrl so you might have to use templateProvider.
$stateProvider.state('home', {
templateProvider: ['$stateParams', 'restService' , function ($stateParams, restService) {
restService.getEntity($stateParams.id).then(function(entity) {
if (entity.Type == 'first') {
return '<div ng-include="first.html"></div>;
} else {
return '<div ng-include="second.html"></div>';
}
});
}]
})
You can also do the following :
$stateProvider
.state('home', {
url : '/{id}',
resolve: {
entity: function($stateParams, RestService) {
return RestService.getEntity($stateParams.id);
}
},
template: 'Home Template <ui-view></ui-view>',
onEnter: function($state, entity) {
if (entity.Type == 'first') {
$timeout(function() {
$state.go('home.first');
}, 0);
} else {
$timeout(function() {
$state.go('home.second');
}, 0);
}
}
})
.state('home.first', {
url: '',
templateUrl: 'first.html',
controller: 'FirstController'
})
.state('home.second', {
url: '',
templateUrl: 'second.html',
controller: 'SecondController'
});
I ended up making the home controller a sibling of first and second, rather than a parent, and then had the controller of home do a $state.go to first or second depending on the results of the resolve.
Use verified code for conditional view in ui-route
$stateProvider.state('dashboard.home', {
url: '/dashboard',
controller: 'MainCtrl',
// templateUrl: $rootScope.active_admin_template,
templateProvider: ['$stateParams', '$templateRequest','$rootScope', function ($stateParams, templateRequest,$rootScope) {
var templateUrl ='';
if ($rootScope.current_user.role == 'MANAGER'){
templateUrl ='views/manager_portal/dashboard.html';
}else{
templateUrl ='views/dashboard/home.html';
}
return templateRequest(templateUrl);
}]
});

Angular UI Router: How to stop reloading based on query parameters change?

Here is my ui-router configuration:
$stateProvider
.state('search.filter.results', {
url: '/results?profileId&keywords',
views: {
'profiles#': {
templateUrl: 'profile-list/profile-list.html',
controller: 'ProfileListCtrl'
},
'profile-summary#search.filter.results': {
templateUrl: 'profile-list/profile-summary/profile-summary.html'
},
'profile-details#search.filter.results': {
templateUrl: 'profile-list/profile-details/profile-details.html',
controller: 'ProfileDetailsCtrl'
}
}
})
ProfileListCtrl has the following method:
$scope.showProfileDetails = function(profileId) {
$state.go('search.filter.results', { profileId: profileId });
};
The problem is, when this method is executed, ProfileListCtrl is instantiated and the view is reloaded.
I would like the reload to happen only if the keywords query parameter changes, but not when the profileId parameter changes.
What would be the best way to achieve this?
I would suggest: split this state into 2 states.
The "parent" keeping the profileId,
the "child" working with keywords.
Until the profileId is not changed, the parent controller won't be re-instantiated.
I created this example with this simplified states definition:
.state('profile', {
url: '/results/:profileId',
views: {
'profiles#': {
//templateUrl: 'profile-list/profile-list.html',
template: '<div>'
...
'<div ui-view=""></div></div>',
controller: 'ProfileCtrl'
},
}
})
.state('profile.results', {
url: '/?keywords',
views: {
'': {
//templateUrl: 'profile-list/profile-list.html',
template: ...
controller: 'ProfileListCtrl'
},
}
})
NOTE: state defintion here is simplified, to just show the split. Not all the details. Also, the child state target parents unnamed view ui-view="", because these should be nested. Finally, parent could even be abstract
This approach also will require some small adjustments of the url. It is not working well if the child url defintion would start with ? like: url: '?keywords'. So I used different pattern:
/results/profileId-value/?kewords=abc

Angular UI-Router: child using parent's view

In story form:
What I am looking for here is a master-detail setup. The master is in list form and when I click on a link (relative to a particular row/record (or Account in this case)) I want to see the details in the main view (literally, the "main" view: <div class="container" ui-view="main"></div>).
I want to do this and maintain my URL structure (/accounts for the list of Accounts; /accounts/:id for the detailed version) but I want the detail view to use the view that the list was using.
What I currently have
index.html
...
<div class="container" ui-view="main"></div>
...
accounts.js
$stateProvider
.state ('accounts', {
url: '/accounts',
views: {
'main': {
controller: 'AccountsCtrl',
templateUrl: 'accounts/accounts.tpl.html'
}
},
data: { pageTitle: 'Account' }
})
.state ('accounts.detail', {
url: '/:id',
views: {
'main': {
controller: 'AccountDetailCtrl',
templateUrl: 'accounts/detail.tpl.html'
}
},
data: { pageTitle: 'Account Detail' }
});
At this point, the /accounts route works as expected. It displays accounts/accounts.tpl.html correctly in the main view. In that html each line in the repeater links it to its appropriate /accounts/:id URL, which I am handling with the nested state accounts.detail.
What is probably obvious to the majority of you who know more than me about this, my accounts.detail will render to the view main if that named view exists in the template accounts/accounts.tpl.html. That is indeed true.
But that is not what I want. I want the accounts.detail stuff to render in the parent main view; I want the html of accounts/detail.tpl.html to replace the html of accounts/accounts.tpl.html found in index.html: <div class="container" ui-view="main"></div>.
So how could I accomplish this?
MY SOLUTION IN CONTEXT
The trick is, as the answer says, to set up the URL scheme to identify which child state is "default". The way I interpret this code in plain English is that the parent class is abstract with the proper URL and the "default" child class has the "same" URL (indicated by '').
If you need further clarity, just post a comment and I'll share any more guidance.
.config(function config( $stateProvider ) { $stateProvider
.state ('accounts', {
abstract: true,
url: '/accounts',
views: {
'main': {
templateUrl: 'accounts/accounts.tpl.html',
controller: 'AccountsCtrl'
}
},
data: { pageTitle: 'Accounts' }
})
.state ('accounts.list', {
url: '',
views: {
'main': {
templateUrl: 'accounts/list.tpl.html',
controller: 'AccountsListCtrl'
}
},
data: { pageTitle: 'Accounts List' }
})
.state ('accounts.detail', {
url: '/:id',
views: {
'main': {
templateUrl: 'accounts/detail.tpl.html',
controller: 'AccountDetailCtrl'
}
},
data: { pageTitle: 'Account Detail' }
});
Sounds like you simply don't want the views to be hierarchical. To do this, simply change the name of the second state to detail.
Note however, that in doing so you will lose any hierarchical properties of the state tree (the controller code state of accounts for example).
If you want to keep the controllers hierarchical, but perform a replace of the html, I would create another parent above both others that takes care of the controller logic, but only has an extremely simple view <div ui-view=""></div>.
For example:
$stateProvider
.state('app', { url: '', abstract: true, template: 'parent.html', controller: 'ParentCtrl' })
.state('app.accounts', { url: '/accounts', templateUrl: 'accounts.tpl.html', controller: 'AccountsCtrl' })
.state('app.detail', { url: '/accounts/:id', templateUrl: 'detail.tpl.html', controller: 'AccountDetailCtrl' });
You can use '#' to define an absolute path to the ui-view of your choice. For example: "detail#contacts" : { }, where this absolutely targets the 'detail' view in the 'contacts' state. within contacts.html
Source: https://github.com/angular-ui/ui-router/wiki/Multiple-Named-Views

Resources