In my AngularJS app using ui-router I have three states. The parent state in the controller resolves a promise and on a successful request, executes the code.
In the child state portfolio.modal.patent, using $stateProvider and the resolve method, I make another request. The issue is that I need to make the resolve method wait until the promise is returned in the parent controller portfolio.
States
$stateProvider
.state('portfolio', {
url: '/portfolio',
templateUrl: 'app/templates/portfolio/portfolio.tpl.htm',
controller: 'portfolioCtrl',
controllerAs: '$ctrl',
})
.state('portfolio.modal', {
abstract: true,
views: {
"modal": {
templateUrl: "app/templates/patent/modal.html"
}
}
})
.state('portfolio.modal.patent', {
url: '/:patentId',
resolve: { //NEED TO MAKE THIS RESOLVE AFTER PORTFOLIO CONTROLLER HAS RETURNED ITS PROMISE
patent: ['$stateParams', 'patentsRestService', function($stateParams, patentsRestService) {
return patentsRestService.fetchPatentItem($stateParams.patentId);
}],
}
}
Portfolio controller
function portfolioCtrl() {
$scope.promise
.then(function(response) {
//WHEN PROMISIS RETURNED, THEN RESOLVE DATA IN PORTFOLIO.MODAL.PATENT
})
}
Question
How do I resolve the child state portfolio.modal.patent data after the promise has been returned from the parent state controller portfolioCtrl
Here is a fast possible workaround how you can try to solve your problem using service as a bridge between requests
Example:
// service
const bridge = $q.defer();
const resolveBridgePromise = data => bridge.resolve(data);
const getBridgePromise = () => bridge.promise;
const fetchDataInParentCtrl = () => {
$timeout(() => {
console.log('data fetched in parent');
resolveBridgePromise({ key: 'fetched data' });
}, 2000);
};
// controller
fetchDataInParentCtrl();
// resolver
getBridgePromise()
.then(data => {
console.log('Do http request here for your child data: ', data);
});
Related
Using AngularJS and ui-router to create states and I have a parent and child state.
.state('portfolio', {
url: '/portfolio',
templateUrl: 'app/templates/portfolio/portfolio.tpl.htm',
controller: 'portfolioCtrl',
controllerAs: '$ctrl'
})
.state('portfolio.patent', {
url: '/:patentId',
views:{
"": {
controller: 'caseOverviewCtrl',
controllerAs: '$ctrl',
templateUrl: 'app/templates/patent/case-overview.tpl.htm',
},
//FURTHER SIBLING VIEWS
}
})
In portfolio in the controller I make a request, await the promise and then display the data to the user in a table. If the user selects an item from the table, it displays further information in a child state portfolio.patent, passing an id value to $stateParams, which I then use to make the $http call to fetch more information.
If I refresh the page, the child state is displaying before the parent state, as the parent state $http request takes longer to resolve as there is a lot more data to fetch. I've tried to check from the child state the value of portfolioLoaded but it only checks it once.
Question
How do I check the parent state promise has resolved, before displaying the child state to the user?
I see using ng-show in the `portfolio.patent' view to check the controller whether the parent promise has been resolved.
PORTFOLIO CONTROLLER
var promise = patentsRestService.fetchAllPatents();
promise.then(
function(response){
var patents = response;
$scope.portfolioLoaded = true;
}
)
PORTFOLIO.PATENT CONTROLLER
function init() {
if($scope.$parent.portfolioLoaded) {
$scope.parentLoaded = true;
}
}
PATENT VIEW
<div data-ng-show="$ctrl.portfolioLoaded" class="animate-show">
//CONTENT
</div>
Save the promise in the parent controller:
$scope.promise = patentsRestService.fetchAllPatents();
$scope.promise.then(
function(response){
var patents = response;
$scope.portfolioLoaded = true;
}
)
Use the promise in the child controller:
function init() {
$scope.$parent.promise.then(function() {
$scope.parentLoaded = true;
});
}
This will properly delay the setting.
Move parent state's call into resolve:
.state('portfolio', {
url: '/portfolio',
templateUrl: 'app/templates/portfolio/portfolio.tpl.htm',
controller: 'portfolioCtrl',
controllerAs: '$ctrl',
resolve: {
patentData: function(patentsRestService){
return patentsRestService.fetchAllPatents()
}
}
})
It will always be resolved before child state loads
Read more: https://github.com/angular-ui/ui-router/wiki#resolve
I'm developing the angular app, using ui - router.
I created root state which is an abstract one aimed to resolve async. dependencies.
So every sub states from my perspective should be able to use those dependencies in own resolve state properties.
So if abstract root state resolving async dependencies and sub state also resolving asyc dependencies, the latter one should wait for the root dependencies to resolved, before starting its own resolve method. Right?
Here is the code example, that shows what I mean:
async, promise based methods that are used inside corresponding resolve
public iAmInTheRootState(): IPromise<any> {
let deferred = this._$q.defer();
this._$timeout(() => {
deferred.resolve();
}, 3000);
return <IPromise<any>> deferred.promise;
}
public iAmInTheSubState(): IPromise<any> {
let deferred = this._$q.defer();
this._$timeout(() => {
deferred.resolve();
}, 100);
return <IPromise<any>> deferred.promise;
}
Root abstract state:
$stateProvider
.state('app', {
url: '/',
abstract: true,
templateUrl: 'layout/app-view.html',
resolve: {
auth: function (Auth: IAuthService) {
'ngInject';
return Auth.iAmInTheRootState().then(() => {
console.log('I am the root state, so I should be first');
});
}
}
});
Sub state which is the daughter state:
$stateProvider.state('app.my-calls', {
url: '',
controller: 'MyCallsController',
controllerAs: '$ctrl',
templateUrl: 'states/my-calls/my-calls.html',
resolve: {
secondAuth: (Auth: IAuthService) => {
'ngInject';
return Auth.iAmInTheSubState().then( () => {
console.log('although I am faster I should be second because i am in the sub state');
});
}
}
})
But the output differs from my expectations:
In your example, the 'app.my-calls' is indeed created after the 'app' state (you can verify this by logging the onEnter callback.)
From Ui-router wiki
Resolve
You can use resolve to provide your controller with content or data that is custom to the state. resolve is an optional map of dependencies which should be injected into the controller.
If any of these dependencies are promises, they will be resolved and converted to a value before the controller is instantiated and the $stateChangeSuccess event is fired.
The resolve callback is not used to delay the state creation, but used to delay the controler creation.
To understand the full flow, you can log the $stateChangeStart & $stateChangeSuccess events.
Although Tim's answer can be considered as a direct response to my question, one may want to understand how to make sub states to wait for their parent's resolve method.
Check out the github issue regarding to this topic.
So in short: sub states should have parent state as a dependency:
Parent State:
$stateProvider
.state('app', {
url: '/',
abstract: true,
templateUrl: 'layout/app-view.html',
resolve: {
ParentAuth: function (Auth: IAuthService) {
'ngInject';
return Auth.iAmInTheRootState().then(() => {
console.log('I am the root state, so I should be first');
});
}
}
});
Child state:
$stateProvider.state('app.my-calls', {
url: '',
controller: 'MyCallsController',
controllerAs: '$ctrl',
templateUrl: 'states/my-calls/my-calls.html',
resolve: {
SubAuth: (ParentAuth, Auth: IAuthService) => {
'ngInject';
return Auth.iAmInTheSubState().then( () => {
console.log('although I am faster I should be second because i am in the sub state');
});
}
}
})
So I have a ui-router state that looks like so:
Parent state
$stateProvider
.state('profile',{
url: '/profile',
views: {
'contentFullRow': {
templateUrl: 'ng/templates/profile/partials/profile-heading-one.html',
controller: function($scope, profile,misc){
$scope.profile = profile;
$scope.misc = misc;
}
},
'contentLeft': {
templateUrl: 'ng/templates/profile/partials/profile-body-one.html',
controller: function($scope, profile,misc){
$scope.profile = profile;
$scope.misc = misc;
}
},
'sidebarRight': {
templateUrl: 'ng/templates/profile/partials/todo-list-one.html',
controller: function($scope, profile,misc){
$scope.profile = profile;
$scope.misc = misc;
}
}
},
resolve: {
profile: function($http){
return $http({method: 'GET', url: '/profile'})
.then (function (response) {
console.log(response.data)
return response.data;
});
},
misc: function($http){
return $http({method: 'GET', url: '/json/misc'})
.then (function (response) {
console.log(response.data)
return response.data;
});
}
}
})
Child states
.state('profile.social', {
url: '/social',
controller:function($scope, profile, misc){
$scope.profile = profile;
$scope.misc = misc;
},
template: '<div ui-view></div>'
})
.state('profile.social.create',{
url: '/create',
onEnter: function($state){
//Will call a modal here...
//How do I access or update `$scope.profile`
//so far am doing this and it works
$state.$current.locals.globals.profile.first_name = 'My New name';
//Is there any better way of doing this?
}
})
Question
Since $scope is not available in onEnter method, how do I access or update $scope.profile
So far am doing something like:
onEnter: function($state){
$state.$current.locals.globals.profile.first_name = 'My New name';
}
This works but am wondering if there is a better way of doing this?
The correct thing to do is not try and access the controllers $scope from outside the controller. You should instead move your profile data to a service, and inject it into both the controller and the onEnter function (as needed). By separating profile data into a service, you can now access it from anywhere else too :)
For example:
.service('ProfileService', function(){
var state = {};
this.loadProfile = function(){
return $http({method: 'GET', url: '/profile'})
.then (function (response) {
console.log(response.data);
state.profile = response.data;
return state.profile;
});
};
this.getState = function(){
return state;
};
});
// the controller
controller: function($scope, ProfileService){
$scope.state = ProfileService.getState();
}
// on enter
onEnter: function($state, ProfileService){
var state = ProfileService.getState();
state.profile.first_name = 'New Name';
}
I wrapped the profile data in a container (state), so that the profile key itself can be changed. So inside your view you will need to reference your profile like so: state.profile.first_name.
Also inside your resolve you will also need to inject the service, and run the load function returning the associated promise (so that resolve actually works).
Without knowing your requirements it is hard to describe the best way to do this, but in summary, you should pull your profile data into its own service, and inject it whenever you need it. The service should also encapsulate any promises that resolve once the service data has loaded.
Using ui-router, I have a state with a resolve function:
.state('tab.social', {
url: '/social/',
views: {
'menuContent': {
templateUrl: 'templates/social/tab-social.html',
controller: 'SocialCtrl',
resolve: {
socialAuthResolve: socialAuthResolve
}
}
}
})
I capture the resolve in the controller as follows:
.controller('SocialCtrl', function($scope, SocialAuth, socialAuthResolve) {
//
console.log(socialAuthResolve);
//
$scope.logOut = function() {
SocialAuth.logOut();
$state.go('tab.social', {}, {reload: true});
};
//
$scope.logIn= function() {
SocialAuth.logIn();
$state.go('tab.social', {}, {reload: true});
};
})
However, when either pressing logOut or logIn, my state is not refreshed. I am using some parameters from the resolve socialAuthResolve and would like this to be updated. In this case, the old parameters are still in there.
Only when I refresh my browser, then the variables and the page are updated accordingly. How can I refresh the page after the logOut and logIn? For instance, force to resolve again?
Here is a sample state with config:
.state('app.stateName', {
url: "/theUrl",
views: {
'myViewName': {
templateUrl: "templates/template.html",
controller: 'SomeController',
resolve: {
pleaseResolve: function() {
console.log("I am resolved");
}
}
}
}
})
In my controller (assuming SomeController as mentioned above), whenever I enter into the state I run this.
var res = ($state.$current.self.views.myViewName.resolve.pleaseReslove)
res.call()
This will call my resolve function every time I come into the view.
$stateProvider
.state('root', {
url: '',
views: {
'root.base': { templateUrl: '/templates/root/root.html' },
'root.sidebar': {
templateUrl: '/templates/root/root-sidebar.html',
controller: 'SomeDataController',
resolve: {
someData: function(DataService) { // DataService is an angular service for data retrieval
return DataService.getDataList(); // returns an array
}
},
}
}
});
If I attempt to run a resolve on a subview with angular-ui-router, the page simply comes up blank. If I omit the resolve, it loads fine -- but I need to resolve for some data before my controller is instantiated. Do I have my syntax correct? I've searched for examples high and low but can't seem to find any that match my situation.
Given my above example, shouldn't the "root-sidebar.html" have the scope of "SomeDataController", and shouldn't SomeDataController have the data resolved in the route definition declared above?
Your concept is working, maybe just some small settings are broken... I created working plunker
Here is the controller consuming someData
.controller('SomeDataController', function($scope, someData) {
$scope.data = someData
})
And here is our service loading some JSON and returning that as async:
.factory('DataService', ['$http',
function($http) {
return {
getDataList: function() {
return $http
.get("data.json")
.then(function(response) {
return response.data;
});
},
};
}
]);
The state defintion is unchanged (just adjusted to display the data):
$stateProvider
.state('root', {
url: '',
views: {
'root.base': {
template: '<div>root base view</div>',
},
'root.sidebar': {
template: '<div>root sidebar view' +
'<h5>the resolve data</h5>' +
'<pre>{{data | json}}</pre>' +
'</div>',
controller: 'SomeDataController',
resolve: {
someData: function(DataService) { // DataService is an angular service for data retrieval
return DataService.getDataList(); // returns an array
}
},
}
}
});
You can test that all here