In the angular ui-router documentation it is stated, that a controller will be instantiated only after all the promises have been resolved, including fetching data from a remote server.
I have a $resource instance:
angular.module('app')
.factory('Item', item);
item.$inject = ['$resource'];
function item($resource) {
return $resource('/items/:id/', {id: '#id'}, {
update: {method: 'PATCH'}
});
}
And I define a state which takes an id from url and retrieves data from a REST API:
$stateProvider.state('item', {
url: '/item/:itemId',
templateUrl: '../components/item/item.html',
controller: 'ItemController',
resolve: {
item: function ($stateParams, Item) {
return Item.get({id: $stateParams.itemId});
}
}
})
In the controller I try to use an Item parameter, assuming that if the controller works, then all the data is already fetched and I can freely use it:
angular.module('app')
.controller('ItemController', ItemController);
ItemController.$inject = ['$scope', 'item', 'Item'];
function ItemController($scope, item, Item) {
$scope.item = item;
$scope.similarItems = Item.query({item_type__id: $scope.item.item_type_detail.id});
}
But the browser gives me this error:
TypeError: Cannot read property 'id' of undefined
And pointing to this exact line, which means that $scope.item.item_type_detail is still undefined, in other words - not resolved.
But if i wrap my second Item call (in the controller) in a $timeout for, say, 5000ms, it would work fine, meaning that the problem is not with the ajax call or improper dependency injection, but that the controller somehow gets instantiated before the promise resolves.
How can this be explained and what can I do to fix it?
Return a promise to the resolver function:
$stateProvider.state('item', {
url: '/item/:itemId',
templateUrl: '../components/item/item.html',
controller: 'ItemController',
resolve: {
item: function ($stateParams, Item) {
//return Item.get({id: $stateParams.itemId});
return Item.get({id: $stateParams.itemId}).$promise;
//return promise --------------------------^^^^^^^^^
}
}
})
It is important to realize that invoking a $resource object method immediately returns an empty reference. Once the data is returned from the server the existing reference is populated with the actual data.
By returning the $promise property, the router will wait for the data from the server.
Related
I want to be able to have exception handling when using getList() or get() however if I user then() nothing useful is returned. If remove then() then it returns what I'm expecting, but I don't have exception handling.
Restangular's own documentation uses then() with getList() so I'm not sure why it's not working.
Here's my controller code:
$scope.allSubjects = Restangular.all('subjects').getList().then(handleSuccess, handleError('Error getting all subjects'));
console.log($scope.allSubjects); // -> Promise {$$state: Object}
function handleSuccess(data) {
return data;
}
function handleError(error) {
return function() {
$log.warn(error);
};
}
However, if I resolve my data it works properly
.state('subjects', {
url: '/subjects',
templateUrl: 'components/subjects/list.html',
controller: 'SubjectsListCtrl',
controllerAs: 'vm',
resolve: {
getSubjectsResolve: function(Restangular) {
return Restangular.all('subjects').getList().then(handleSuccess..., handleError...);
}
}
})
I'm so lost! I don't get it.
In your handleSuccess, you are returning the data. Who are you returning it to? Typically, your success function would look like:
function handleSuccess(response) {
$scope.data = response.data;
}
I use UI router's resolve object to add a dependency (a promise) to a controller. On initial page load, this gets resolved as intended and the object in the controller is populated with the data from the dependency. I use this object in the view to output a ng-repeat directive.
In another state, I have a function that adds to the model (the same that is getting resolved on the previous state). Once added, I transition the state back to the previous state with $state.go().
As the resolve's method fetches the model and injects the dependency into the previous controller, I expect that the ng-repeat output should include the newly added item. But it doesn't. Only when refreshing does the view get updated.
From console.loging in the resolve method, I can see that the method is being called and that the data has been fetched.
I placed a $watch on the dependency to update the controller's object with the new values, but that watch doesn't get called. Why does the $watch not see the change in the dependency, when it has clearly changed?
Route
angular.module('app')
.config(function($stateProvider, $urlRouterProvider) {
$stateProvider
.state('tab', {
url: "/tab",
abstract: true,
templateUrl: "templates/tabs.html"
})
.state('tab.queries', {
url: '/queries',
views: {
'tab-queries': {
templateUrl: 'templates/tab-queries.html',
controller: 'QueriesCtrl as ctrl'
}
},
resolve: {
allTemplates: function(Templates) {
return Templates.all().then(function(templates) {
console.log(templates); // This outputs with the correct data every time the state is entered
return templates;
});
}
}
})
.state('tab.guide', {...}); // controller: GuideCtrl as ctrl
});
QueriesCtrl
function queriesCtrl($scope, allTemplates) {
var vm = this;
vm.queries = allTemplates;
$scope.$watch('allTemplates', function() {
// Called on first load, not on subsequent state changes
console.log('templates changed');
vm.queries = allTemplates;
});
}
GuideCtrl
function myCtrl($state) {
var vm = this;
vm.goToQueries = function() {
/* -> Code saving to the model here <- */
$state.go('tab.queries');
}
}
View (tab.queries)
<div ng-repeat="query in ctrl.queries">
...
</div>
In one of my UI-router states, I have the following link '/users', which points to a page that shows a list of users. Below is the code I wrote to resolve the list of users by using a $resource call, however, the data doesn't seem resolved when the page is loaded:
.state('users', {
url: '/users',
templateUrl: '/assets/angularApp/partials/users.html',
controller: 'UserListCtrl as vm',
resolve: {
users: function ($resource) {
return $resource('http://localhost:8080/api/users').query().$promise.then(function(data) {
return data;
});
}
}
})
In the UserListCtrl, I have the following to assign the resolved users to vm.users so that the users can be displayed on the partial page:
function UserListCtrl($log, users) {
var vm = this;
vm.users = users;
}
The page, however, only displays a few empty rows without any data filled in. So I think the resolve is not working properly on my url /users, could anyone spot where might be problematic? Thanks
Change it to:
users: function ($resource) {
return $resource('http://localhost:8080/api/users').query().$promise;
}
Because that way you're returning the promise, and by using then(...), you're resolving the promise within and just returning data thus it won't pass it to the controller because it's returning them after the controller has loaded.
Using $q service will help you
var userVal = $resource('http://localhost:8080/api/users').query()
retrun $q.resolve(userVal.$promise).then(function(r){ return r;});
Inside you controller, you are good to go
vm.users = users
I have an ngResource object like this:
[...].factory('Event', ['$resource', function ($resource) {
return $resource('/events/:id', {id: '#id'}, {
resume: {url: '/events/:id/resume'},
signUpload: {url: '/events/:id/sign-upload'},
});
}]);
But when I call myModel.$resume(); or myModel.$signUpload() the returned data gets automatically saved to my model. However, the returned data is not my model attributes, but actually another completely different return.
I need to avoid auto-saving the returned data from the server. Is there anything out-of-the-box to do that? I couldn't find it here: https://docs.angularjs.org/api/ngResource/service/$resource
Thanks
For this case you can try to not use resource, but create service.
app.service('eventService', ['$http, $q', function ($http, $q) {
this.signUpload = function(eventId) {
var defer = $q.defer();
$http.get('/events/' + eventId + '/sign-upload')
.then(function(result) {
defer.resolve(result.data);
})
.catch(function(err) {
defer.reject(new Error(err));
});
return defer.promise;
}
// same for other function
}]);
Inject this service in controller, and just do eventService.signUpload(eventId);
I've built a table with a filter, sorting, limit and pagination system.
With normal array of objects from the javascript works as expected.
My problem comes when I try to retrieve data from the server and to use it with my client-side pagination and filter. When the page is load for a few seconds the $scope.data is undefined until $http is resolved. I don't know how to solve it and I hope you can help me.
This is the pagination filter which I think is initialized before $http is resolve.
.filter('pagination', function () {
return function (inputArray, currentPage, pageSize) {
var start = (currentPage - 1) * pageSize;
return inputArray.slice(start, start + pageSize);
};
})
I will leave a fiddle here of my problem so you can see it there better.
Fiddle
I don't know if I should edit my post or I do it correctly posting an answer but I came up with a solution and I want to share it.
My problem was that the $scope.data (request from the server) was undefined when controller was initialized so what I did is to set up a route and wait for the promise to resolve before initializing the controller.
One problem I came up with is the fact that it's not possible to inject a .factory directly into the .config so I had to inject $provide to my .config and provide it with my service.
This is the a part of the code I'm talking about :
.config(['$routeProvider','$provide', function($routeProvider, $provide) {
$provide.factory('select', ['$http','$q', function($http, $q) {
return {
getItems : function () {
var deferred = $q.defer();
$http.get('php/select_diarios.php', {cache : true}).success(function (data) {
//Passing data to deferred's resolve function on successful completion
deferred.resolve(data);
}).error(function () {
//Sending an error message in case of failure
deferred.reject("An error occured while fetching items");
});
//Returning the promise object
return deferred.promise;
}
}
}]);
$routeProvider
.when('/', {
templateUrl: 'modulos/home/home.html',
controller: 'AppController',
resolve: {
**data**: function (select) {
return select.getItems();
}
}
}).
when('/genericos', {
controller: 'genericosController'
})
.otherwise({ redirectTo: '/' });
}])
Finally I only had to inject the data parameter of the resolve into my controller like so:
.controller('AppController', ['$scope', '$location','filterFilter','**data**', function ($scope, $location, searchFilter, **data**)
... more code
}]);
Also because of the otherwise redirect you must set a <a href="#/></a> in the pagination cause if not, each time you click to switch a page it doesn't know where to go and redirects to the first one and never changes the page.
this is how the pagination look like.
<ul class="pagination pull-right">
<li>«</li>
<li ng-repeat="n in range" ng-class="{active: n === (currentPage -1)}" ng-click="setPage(n+1)">{{n+1}}</li>
<li>»</li>
</ul>