All over my code I have things like:
SearchModel.findAll($scope.report).then(function (xhr) {
$scope.searchResults= xhr.data;
});
is there anyway to just automagically assign the searchResults variable to the view after the request is done. Seems like there should be if not...
Promises are only resolved during a $digest cycle, so this should "automagically" update.
It really is that easy!
To prove it I made a fiddle that simulates a server response using a service and assign the data to a scope. The dom will automatically display the data.
service.get().then(function(data) {
$scope.data = data;
});
Hope this helped!
If you are dealing with a promise that returns an array, you can use a helper function like this:
app.factory('PromiseList', function() {
return function(promise, error) {
var list = [];
promise.then(function (result) {
angular.copy(result, list); # or in your case .copy(result.data, list)
}, error);
return list;
}
});
Then in your code, do:
$scope.searchResults = PromiseList(SearchModel.findAll($scope.report))
You can also use this solution with other kinds of objects, however not with strings or numbers as they are immutable objects in JavaScript.
If you are only developing for an old version of AngularJS (think pre-1.2), you can also just pass the promise to the template and it will be unwrapped automatically.
Related
I have a method in my angular 1.5 controller, as shown below but I wanted to refactor the ajax call into the factory itself but I'm having problems with promises.. I'm trying to get to a point where in my controller I can just call the method like I've shown below. Is this possible? I'm trying to avoid having the ...success(function(...) in the controller code.
Any help much appreciated.
Trying to move to
vm.member = someFactory.getMember(vm.id);
Existing working controller code
vm.myMethod = myMethod;
...
function myMethod() {
someFactory.getMember(vm.id).success(function(response) {
vm.member = response;
});
}
When I move the getMethod line into the factory the response is populated obviously but as soon as I come back to the controller, even with the return value from the factory being the response the result is undefined. I know this is because of promises but is there a design pattern I'm missing or a clean way of doing this. Using my currently approach my controller is littered with .success(function()...)
Many thanks!
The procedure is called promise unwrapping.
Besides the fact that success is deprecated and should be replaced with then,
someFactory.getMember(vm.id).then(function(response) {
var data = res.data;
...
});
it is totally ok to have this in controller.
The alternative to this pattern is to return self-filling object (something that ngResource $resource does):
function getMember(...) {
var data = {};
$http(...).then(function (response) {
// considering that response data is JSON object,
// it can replace existing data object
angular.copy(data, response.data);
});
return data;
}
In this case controller can get a reference to the object instantly, and the bindings of object properties {{ vm.member.someProperty }} will be updated in view on response.
The pattern is limited to objects (and arrays), scalar values should be wrapped with objects.
How can I make an Angular service code "look synchronous"?
My questions arose when I cleaned my controller and put the business logic code into a service instead. So far so good. Now I would like to "wait" in the service function until all asynchronous calls have returned and then return. How can I do that?
To illustrate my problem, suppose you have a controller code which just:
requests some data from the backend
does some processing with the data and
hands the data over to the scope
Like that:
DataController before refactoring:
$scope.submitForm = function() {
RestBackend.query('something').then(function(data) {
// do some additional things ...
...
$scope.data = data;
});
};
Pretty straightforward. Fetch data and fill scope.
After refactoring into controller + service, I ended up with:
DataController refactored:
$scope.submitForm = function() {
DataService.getData().then(function(data) {
$scope.data = data;
});
};
DataService refactored:
this.query = function() {
var dataDefer = $q.defer();
RestBackend.query('something').then(function(data) {
// do some additional things ...
...
dataDefer.resolve(data);
});
return dataDefer.promise;
};
I dislike the fact that I have to work with a promise in the controller also. I like promises but I want to keep the controller agnostic of this "implementation detail" of the service. This is what I would like the controller code to look like:
DataController (as it should be):
$scope.submitForm = function() {
$scope.data = DataService.getData();
};
You get the point? In the controller I don't want to care about promise or not. Just wait for the data to be fetched and then use it. Thus, I am looking for a possibility to implement the service like this:
query the data (asynchronously)
do not return until the data has been fetched
return the fetched data
Now item 2. is not clear to me: How can I "wait until data has been fetched" and only proceed afterwards? The goal is that the service function looks synchronous.
I too think your solution is fine.
Returning a promise is not an implementation detail of the service. It is part of the service's API (the "contract" between the service and the service-consumer).
The controller expects a promise that resolves with the data and handles that as it sees fit.
How that promise is constructed, how the data is fetched etc, these are the implementation details.
You can swap the service at any time with one that does totally different things as long as it fulfills the contract (i.e. returns a promise that resolves with the data onve ready).
That said, if you only use the data in the view (i.e. do not directly manipulate it in the controller right after it is fetched), which seems to be the case, you can use ngResources approach:
Return an empty array and populate it with the data once it is fetched:
$scope.data = DataService.getData();
// DataService refactored:
this.getData = function () {
var data = [];
RestBackend.query('something').then(function(responseData) {
// do some additional things ...
...
angular.forEach(responseData, function (item) {
data.push(item);
});
});
return data;
};
BTW, in your current (fine) setup, you need $q.defer(). You can just use promise-chaining:
this.query = function() {
return RestBackend.query('something').then(function(data) {
// do some additional things ...
...
return data;
});
};
I think what you have is a very good solution. You should not have to wait for promise to be resolved, it defeats the purpose of async javascript. Just ask yourself why do you need to make it run sync?
If you rely in html on promise to be resolve you can do something like this
<div class="alert alert-warning text-center" data-ng-hide="!data.$resolved">
Got data from service.
</div>
As you use ngRoute, I would recommend you to resolve you data in your route config, and the view will be loaded once all your data will be resolved.
$routeProvider
.when('/your-url', {
templateUrl: 'path/to/your/template.html',
controller: 'YourCtrl',
// that's the point !
resolve: {
superAwesomeData: function (DataService) {
return DataService.getData();
}
}
});
Now, superAwesomeData can be injected in your controller and it will contains the data, resolved.
angular.module('youModule')
.controller('YourCtrl', function (superAwesomeData) {
// superAwesomeData === [...];
});
I want to create an angular service that grabs data at "start-up" and builds up an array that will not change for the lifetime of the application. I don't want to return the promise of the http request. Caching the result would still incurs the cost of the 'someExpensiveFunctionCall'. What's the best way to handle this situation that can guarantee that the service will have the result populated and can be used in a controller and/or another service.
angular.module('app')
.service('MyService', function($http, defaults) {
var result = [];
$http.get('/some/url').then(function(data) {
if(angular.isDefined(data)) {
_.forEach(data, function(item) {
result.push(someExpensiveFunctionCall(item.value));
});
} else {
result = angular.copy(defaults.defaultResult);
}
});
return {
result: result
};
})
.controller(function(MyService) {
var someData = MyService.result;
})
.service(function(MyService) {
var someData = MyService.result;
});
I don't want to return the promise of the http request.
It seems like you want to do something that is asynchronous; synchronously. This can't work (reliably) without some sort of callback, promise, sync XHR, or other mechanism.
Knowing that we can evaluate some options. (in order of most practical)
Use your routers resolve property. This will complete some promise returning operation before instantiating your controller. It also (in ui-router and ngRoute) means the result of the promise will be injected into the controller under the specified key name.
Use the promise.
(aka #1 manually)
.factory('MyService',function($http,defaults){
var ret = {result: []};
ret.promise = $http.get('url').then(function(data){
if(angular.isDefined(data)){
_.forEach(data,function(item){
ret.result.push(someExpensiveFunctionCall(item.value));
});
}else{
_.forEach(angular.copy(defaults.defaultResult),function(val){
ret.result.push(val);
});
}
return ret;
});
return ret;
})
.controller('MyCtrl',function(MyService){
MyService.promise.then(myCtrl);
function myCtrl(){
var result = MyService.result
}
})
Lazy Evaluation. --> this is a little slow in angular due to dirty checking. But essentially means that you don't immediately manipulate/read result inside the body of a controller or service. But instead, use the view bindings, filters and interactions to call scope functions that act on the data. Since the resolution of a promise will invoke a digest cycle. Your view will render when the data arrives.
There was a bug in your code if the data happens to be undefined. You are de-referencing result by setting it to a copy of the defaults.
So all references to MyService.result will always be an empty array and never the default value.
In order to maintain the reference I'd loop over the defaults and add them to result with result.push.
Hope this helps!
I'm trying to get my head around sharing data between multiple controllers, but couldn't find out yet how this is supposed to work (the angular way). I have create a Data service that look something like this:
angular.module('myapp.services')
.service('DataSet', function($rootScope) {
return {
filter: function(filterMethod) {
/// ... do async stuff
$rootScope.$broadcast("Data::filtered");
},
brush: function(brushed) {
/// ... do async stuff
$rootScope.$broadcast("Data::brushed");
},
load: function() {
/// ... do async stuff
$rootScope.$broadcast("Data::loaded");
}
};
});
Next I want to reuse and update data from this service, so I use it in my controller as follows:
angular.module('myapp.controllers')
.controller('FilterCtrl', function ($scope, $rootScope, DataSet) {
$scope.safeApply = function(fn) {
var phase = this.$root.$$phase;
if(phase == '$apply' || phase == '$digest') {
if(fn && (typeof(fn) === 'function')) {
fn();
}
} else {
this.$apply(fn);
}
};
function updateBrushed() {
$scope.safeApply(function() {
$scope.brushed = DataSet.brushed;
});
};
$scope.brushed = [];
$scope.keepSelected = function() {
DataSet.filter(DataSet.FilterMethod.KEEP);
};
$scope.removeSelected = function() {
DataSet.filter(DataSet.FilterMethod.REMOVE);
};
$scope.$on('Data::brushed', updateBrushed);
$scope.$on('Data::filtered', updateBrushed);
});
The problem I have is basically illustrated by the use of the saveApply call. Basically I got this code from here: https://coderwall.com/p/ngisma. What I don't understand though is why I need it. As far as I can see, I'm 'within' $angular when updating the DataSet service. Nevertheless, the view for the Filter controller doesn't get updated without a call to saveApply ($apply doesn't work at all because than I run into the apply already in progress issue).
So, basically my question boils down to: is the approach above a good way to share data, and if so how is notification of changes in the service supposed to work?
Update: Based on Julian Hollman his suggestion I came to the following solution: http://jsfiddle.net/Ljfadvru/7/. This more or less illustrates the full workflow I was working on, though some of it is automatically induced in the fiddle, as opposed to user-interaction based in my real application. What I like about this approach is that it only sends signals when all data is updated.
Working with references, as suggested by Ed Hinchliffe, is nice as well. However, I'm working on a web visualization framework and I'm expecting tens of thousands of items. Clearing arrays and pushing new elements (which seem to me the consequence of this proposal) is really not feasible (if I understand this paradigm well, it would also result in a re-rendering of my vis for every single change). I stand corrected though if there are suggestions for further improvement.
$broadcast doesn't trigger an $apply and I bet your "async stuff" is not $http from angular.
So something happens outside of angular and angular doesn't know that something has changed.
In my opinion the best thing in that case is to write a wrapper for your async code and trigger $apply after date came back from the backend. Don't do it in the controller.
To be honest, I'm not sure quite sure about exactly what is going on with the digest loops in your particular scenario, but I don't think you are approaching this the right way.
The 'angular' way, is to use promises.
Your service should be more like this:
angular.module('myapp.services')
.service('DataSet', function($rootScope) {
return {
filter: function(filterMethod) {
var returnData = []
$http.get('/some/stuff').then(function(data){
for(i in data){
returnData.push(data[i]);
}
});
return returnData;
}
};
});
This sets up an empty placeholder object (returnData) that can be immediately passed to the controller, but a reference is kept so that when the data returns you can retrospectively populate that object. Because the controller and the service reference the same object, it'll 'just work'.
This way you don't have to worry about dealing with $digest or $apply or $broadcast.
You controller can just call $scope.filtered = DataSet.filter();
EDIT
If you want to be able to access the exact same data from multiple controllers:
angular.module('myapp.services')
.factory('DataSet', function($http) {
var cache = {
filtered: []
}
return {
getFiltered: function(){
if(cache.filtered.length) return cache.filtered;
$http.get('/some/url/').then(function(data){
for(i in data){
cache.filtered.push(data[i]);
}
});
}
};
});
I'm working with a REST api that provides a paginated response for GET requests, like so:
{count: 43103
previous: null
next: http://ecoengine.berkeley.edu/api/photos/?page=2
results: [json objects....]
}
I would like to create a service that loads all the data by following the next link till next becomes null. I'm stuck on how to chain promises in this scenario and would appreciate any help on how to proceed (angular/js newbie here). My plunker with where i've gotten so far is here http://plnkr.co/edit/ySiQLvu9RNrKkQAoDmKh. You can see from the console messages that the code retrieves data from first 2 pages only. Thank you.
I tried to do chaining of promises using recursion to solve this scenario. See my fiddle here
http://plnkr.co/edit/NPh6uQ2DgVuhVxUgHB6h?p=info
Basically recursion done on loadData can get paged data. This is the implementation
var loadData = function(url) {
var deferred = $q.defer();
function loadAll() {
$http.get(url)
.then(function(d) {
debugger;
console.log('private http.get().then()');
console.log(d);
aggregateData.value.push(d.data.results);
if(d.data.next) {
url=d.data.next;
loadAll();
}
else {
deferred.resolve(aggregateData.value);
}
})
}
debugger;
loadAll();
return deferred.promise;
};
I used the aggregateData array but you are free to use any array declared in the loadData function.