I am having problem with setting up a service that will perform asynchronous http request.
Background
I have a backend endpoint /cars.json When you hit this endpoint for the first time the background job which will fetch all the cars information gets started. Server then returns response with status 200:
{"status":"started", "percentage_completion":0, "data":null, "error":null}
Consecutive requests to this endpoint will keep on returning status 200 with updated percentage_completion
{"status":"started", "percentage_completion":45, "data":null, "error":null}
{"status":"started", "percentage_completion":90, "data":null, "error":null}
Finally the request to the endpoint will return:
{"status":"finished", "percentage_completion":100, "data": {...}, "error":null}
and happy days I have a data and I can add it to the $scope.
But I am having hard time to get my head around the promises and angular way of doing things...
I have spiked something like this:
var dashboard = angular.module('dashboard', [
'templates'
]);
dashboard.controller('dashboardController', ['carsService', '$scope',
function(carsService, $scope){
$scope.test = "Loading...";
carsService.get()
.then(function(response){
$scope.test = response.data;
});
}]
);
dashboard.factory('carsService', ['$http', function($http){
var get = function(){
return $http.get('/cars.json');
};
return {
get: get
};
}]);
It sends a single request to the server and updates the test on the scope with the first response stating the job has been started. When I refresh the page after few seconds I get the test updated with a correct data and the finish status.
What would be a best way, angular way:) with promises and stuff to have the service and controller do it automatically.
First thing that comes to my head is to use a $interval or something similar and periodically resend the request from .then(success) but maybe there is a better way.
Angular documentation says something about progress/notify promise but at the moment I have no idea how to wire it in.
Obviously I don't fully understand the promises at the moment, I just got introduced to them - can you share some tips/resources on how to deal with asynchronous requests using angular?
I am not positive that I understand your question fully, but if you are wanting to get the progress updates as they come back, Angular has a progress callback built into their promise service (as you mention). The first callback inside your .then() is your success callback, the second is your error callback, and the third would be your progress callback.
So when chaining your callbacks onto the promise, you can use the following syntax to update a progress indicator:
carsService.get()
.then(function(response){
// success
$scope.test = response.data;
$scope.$apply(); // or wrapped in $timeout
}, function(error){
// error
}, function(percentComplete){
// progress
$scope.progress = percentComplete;
$scope.$apply(); // or wrapped in $timeout
});
As to the best syntax to use when writing and chaining your promises, John Papa has a fantastic (and very in-depth) style guide that will help you better organize your Angular code. You can find this here.
Here's a sample jsfiddle demonstrating this notify/progress callback in action, as well. It is using a timeout to simulate the async call, but displays the general idea well.
Also remember that when you make these async calls, you may need to run $scope.$apply() or wrap the code in the callback where you update your scope data inside of a $timeout(function(){ //update data }); in order to run the Angular digest cycle again (async calls update your data outside of Angular's built-in watchers). This will then cause your view to update, as well, without having to reload a page. There are quite a few blog posts and SO questions regarding this that you can use if you've never run into this behavior before. Here's one that covers it pretty well.
I guess you can use nested promise to achieve progress notify:
function get(){
var defer = $q.defer();
(function fetchData(){
$http.get('/cars.json').then(function(result){
if(result.status === 'started'){
fetchData();
defer.notify(result);
}else if(result.status === 'finished'){
defer.resolve(result);
}
}, function(err){
defer.reject(err);
})
})()
return defer.promise;
}
when you call get(), it first call fetchData to request data from server. When fetchData resolved, check result.status, if status is not finished, call fetchData again and notify outter defer, otherwise, resolve outter defer with final result
//in controller
carService.get().then(
function(finalData){ $scope.test = finalData },
function(err){ ... },
function(notifyData){ $scope.test = notifyData }
}
All the credit goes to MarkoCen for his suggestion on using chained promises. This is my final solution:
app.js.coffee
angular
.module 'dashboard', ['templates']
car_service.coffee
CarService = ($q, $http)->
get = (url,defer)->
$http.get(url).then(
(result)->
data = result.data
if data.status == "started"
get(url, defer)
defer.notify(data.percentage_completion)
else
defer.resolve(data.data.cars)
)
defer.promise
return{
getAll: (url)->
defer = $q.defer()
get(url,defer)
}
angular
.module 'dashboard'
.factory 'CarService', [
'$q'
'$http'
CarService
]
cars_controller.coffee
DashboardController = (CarService) ->
vm = #
vm.campaigns = []
vm.statusBar = {}
CarService.getAll('/cars.json').then(
(data)->
vm.campaigns = data
(reason)->
console.log reason
(update)->
vm.statusBar.percentage = update
)
return
angular
.module 'dashboard'
.controller 'DashboardController', [
'CarService'
DashboardController
]
Here there is a reference about $http service in AngularJS.
https://docs.angularjs.org/api/ng/service/$http
This is the explanation about the $http service and how to use then()
function.
// Simple GET request example:
$http({
method: 'GET',
url: '/someUrl'
}).then(function successCallback(response) {
// this callback will be called asynchronously
// when the response is available
}, function errorCallback(response) {
// called asynchronously if an error occurs
// or server returns response with an error status.
});
The best way is using HTML5 Server-Sent Events or WebSockets technology where you can get data in realtime.
In this demo I'm using anonymous function inside the then() function.
I've made two very simple demos by using PHP, Server Sent Events, JSON and AngularJS Factory service.
AngularJS Factory Service and PHP JSON Response: cars.php
<?php
header("Access-Control-Allow-origin: *"); // To allow cross-origin HTTP request.
header("Content-Type: application/json"); // JSON response.
header("Cache-Control: no-cache"); // No cache.
/*
Only to display random numeric values.
In a real scenario the data can be obtained from database.
*/
$min = 0;
$max = 100;
$count = rand($min, $max); // Get a random numeric value.
$status = "started";
if($count == $max)
{
$status = "finished"; // If the $count == 100 then finished.
}
// Builing an array with current data.
$array = array("status" => $status, "percentage_completion" => $count, "data" => null, "error" => null);
echo json_encode($array); // Encode the array to the json representation.
?>
(function() {
var dashboard = angular.module("dashboard", []);
dashboard.controller("dashboardController", ["carsService", "$scope",
function(carsService, $scope) {
$scope.statusJSON = "Loading...";
$scope.testJSON = {};
$scope.initJSON = function() {
(function loop() { // Looping function to continous request.
carsService.get().then(function(response) { // Angular promise by using then() function.
$scope.testJSON = response.data;
$scope.statusJSON = $scope.testJSON.status;
console.log($scope.testJSON);
}, function(response) {
console.log("Error: " + response.status);
});
setTimeout(loop, 1000); // Call the $http service every 1 second.
})();
};
}
]);
dashboard.factory("carsService", ["$http",
function($http) {
return { // This factory service returns an object with the get function.
get: function() {
return $http.get("http://dfjb.webcindario.com/cars.php", {
responseType: "json"
}); // Returns a $http service.
}
};
}
]);
})();
.json {
border: solid 1px #444444;
margin: 5px;
padding: 5px;
}
<html data-ng-app="dashboard">
<head>
<title>Demo AngularJS</title>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
</head>
<body data-ng-controller="dashboardController">
<div class="json" data-ng-init="initJSON()">
<h3>Demo With Angular $http Service</h3>
<div data-ng-bind="statusJSON"></div>
percentage_completion: {{testJSON.percentage_completion}}
<br />
<progress min="0" max="100" value="{{testJSON.percentage_completion}}" />
</div>
</body>
</html>
HTML5 Server-Sent Events: data.php
<?php
header("Access-Control-Allow-origin: *"); // To allow cross-origin HTTP request.
header("Content-Type: text/event-stream"); // To send event streams.
header("Cache-Control: no-cache"); // No cache.
/*
Only to display random numeric values.
In a real scenario the data can be obtained from database.
*/
$min = 0;
$max = 100;
$count = rand($min, $max); // Get a random numeric value.
$status = "started";
if($count == $max)
{
$status = "finished"; // If the $count == 100 then finished.
}
// Builing an array with current data.
$array = array("status" => $status, "percentage_completion" => $count, "data" => null, "error" => null);
echo "data: ". json_encode($array) ."\n\n"; // Encode the array to the json representation. The event-stream always start with "data: ".
flush(); // Flush the output data back to the web page.
?>
(function() {
var dashboard = angular.module("dashboard", []);
dashboard.controller("dashboardController", ["carsService", "$scope",
function(carsService, $scope) {
$scope.statusSSE = "Loading...";
$scope.testSSE = {};
$scope.initSSE = function() {
// Check if EventSource is supported by the browser.
if (typeof(EventSource) !== "undefined") {
carsService.getDataSSE().onmessage = function() {
// The factory service has a function that returns an EventSource object, so this can be accessed in the controller.
$scope.testSSE = JSON.parse(event.data); // Parse the string in an object.
$scope.statusSSE = $scope.testSSE.status;
$scope.$apply(); // Update the $scope variable so can be used in the view.
console.log($scope.testSSE);
};
} else {
alert("SSE not supported by browser.");
}
};
}
]);
dashboard.factory("carsService", [
function() {
return {
getDataSSE: function() {
// HTML5 Server-Sent Events Implementation.
return new EventSource("http://dfjb.webcindario.com/data.php"); // Returns the EventSource object.
}
};
}
]);
})();
.sse {
border: solid 1px #FF44AA;
margin: 5px;
padding: 5px;
}
<html data-ng-app="dashboard">
<head>
<title>Demo AngularJS</title>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
</head>
<body data-ng-controller="dashboardController">
<div class="sse" data-ng-init="initSSE()">
<h3>Demo With HTML5 Server-Sent Events in PHP</h3>
<div data-ng-bind="statusSSE"></div>
percentage_completion: {{testSSE.percentage_completion}}
<br />
<progress min="0" max="100" value="{{testSSE.percentage_completion}}" />
</div>
</body>
</html>
Finally, in this demo, I'm showing you how to use realtime notifications from server by using AngularJS Factory service in PHP and HTML5 Server-Sent Events Technology with json response.
If you check the console you get a json string in the EventStream tab automatically.
Related
I am really new to angularJS. I need to develop a page where angular JS wait for a event to happen at server side so angular JS should keep checking server using $http call in every 2 seconds. Once that event completes Angular should not invoke any $http call to server again.
I tried different method but it gives me error like "Watchers fired in the last 5 iterations: []"
Please let me know how to do it.
Following is my code
HTML
<div ng-controller="myController">
<div id="divOnTop" ng-show="!isEventDone()">
<div class="render"></div>
</div>
</div>
Angular JS
var ngApp = angular.module("ngApp",[]);
ngApp.controller('myController', function ($scope, $http) {
$scope.ready = false;
$scope.isEventDone = function () {
$scope.ready = $scope.getData();
return $scope.ready;
};
$scope.getData = function () {
if (! $scope.ready) {
$http.get("/EventManager/IsEventDone")
.then(function (response) {
$scope.ready = Boolean(response.data);
});
}
};
setInterval($scope.isPageReady, 5000);
});
A few things here.
I'm not convinced the accepted answer actually works nor solves the initial problem. So, I'll share my 2 cents here.
$scope.ready = $scope.getData(); will set $scope.ready to undefined each time since this method doesn't return anything. Thus, ng-show="!isEventDone()" will always show the DOM.
You should use angular's $interval instead of setInterval for short-polling in angular.
Also, I've refactored some redundancy.
var ngApp = angular.module("ngApp",[]);
ngApp.controller('myController', function ($scope, $http, $interval) {
var intervalPromise = $interval($scope.getData, 5000);
$scope.getData = function () {
if (! $scope.isEventDone) {
$http
.get("/EventManager/IsEventDone")
.then(function (response) {
$scope.isEventDone = Boolean(response.data);
if($scope.isEventDone) {
$interval.cancel(intervalPromise);
}
});
}
else {
$interval.cancel(intervalPromise);
}
};
});
This should work and solve your initial problem. However, there's a scenario where your server may be on a high load and takes 3 seconds to respond. In this case, you're calling the server every 2 seconds because you're waiting for 5 seconds after the previous request has started and not waiting for after the previous request has ended.
A better solution than this is to use a module like async which easily handles asynchronous methods. Combining with $timeout:
var ngApp = angular.module("ngApp",[]);
ngApp.controller('myController', function ($scope, $http, $timeout) {
var getData = function(cb){
if(!$scope.isEventDone) return cb();
$http.get("/EventManager/IsEventDone")
.then(function (response) {
$scope.isEventDone = Boolean(response.data);
cb();
});
};
// do during will run getData at least once
async.doDuring(getData, function test(err, cb) {
// asynchronous test method to see if loop should still occur
// call callback 5 seconds after getData has responded
// instead of counting 5 seconds after getData initiated the request
$timeout(function(){
cb(null, !$scope.isEventDone);
// if second param is true, call `getData()` again otherwise, end the loop
}, 5000);
}, function(err) {
console.log(err);
// if you're here, either error has occurred or
// the loop has ended with `$scope.isEventDone = true`
});
});
This will call the timeout after the request has ended.
A better alternative, if you have control of the server, is to use a websocket which will enable long-polling (server notifies the client instead of client making frequent requests) and this will not increase significant load on the server as clients grow.
I hope this helps
In your example $scope.pageIsReady does not exist. What you could do is inject the $timeout service into your controller and wrap your http call inside of it:
var timeoutInstance = $timeout(function(){
$http.get("/EventManager/IsEventDone")
.then(function (response) {
$scope.ready = Boolean(response.data);
if($scope.ready){
$timeout.cancel(timeoutInstance);
else
$scope.getData();
}
});
},5000);
cancel will stop the timeout from being called. I have not tested this but it should be along those lines.
Also not sure what type of backend you are using but if it is .net you could look into SignalR which uses sockets so the server side tells the front end when it is ready and therefore you no longer need to use polling.
In angularJS, I have written a directive to display data. This data is fetched by a http service from remote database.
If I apply below timeout service in code, then result is displayed on the html page as http service returns in one second.
$timeout(function () {
$scope.treeRoot = $scope.$eval('serviceResult') || {};
}, 1000);
If i do not use timeout service, then page does not show any data because $scope.treeRoot is empty e.g.
$scope.treeRoot = $scope.$eval('serviceResult') || {};
In production, the http service can take time more than one second to return, is there any generic solution.
You should use Angular's promises or $http service which allows to return a promise.
After the promise, e.g. $http request, is resolved, you can populate $scope.treeRoot with the results.
Let me know if that helps.
AS suggested in one answer you have to use the $http promises and capture the result in response.
Here is an example with some code.
Service:
app.factory('myService', ['$http',
function($http){
var obj = {};
obj.get = function (q) {
return $http.get('/your_get_url')
};
obj.post = function (q, object) {
return $http.post('/your_post_url', object)
};
return obj;
}
]);
In your controller,
allControllers.controller('changePasswordController', ['$scope','myService',
function($scope,myService) {
myService.get().then(function (result) {
$scope.treeRoot = 'use your result here'
},function error(result)
{
console.log(result)
})
}
])
This replaces your timeout issue.
I have trouble fetching one unique item from my firebase using angularfire 1.0.0. To clarify, I want my app to fetch a post given a unique firebase id e.g. "-JkZwz-tyYoRLoRqlI_I". It works when navigating in the app e.g. clicking on a link to a specific post, but not on a refresh. My guess is that it has something to do with synchronization. Right now it works when fetching all posts and use it in a ng-repeat. This is a clue to why it works for one item when navigating to the page. This should probably not be hard since this should be a pretty standard operation, but i can't get it to work. I have searched everywhere but there is actually no guide on this. In the API they refer to $getRecord(key)
Returns the record from the array for the given key. If the key is not
found, returns null. This method utilizes $indexFor(key) to find the
appropriate record.
But this is not working as expected. Or am i missing something?
It works for ng-repeat like this:
<div ng-repeat="postt in posts">
<div>
<h1>{{postt.title}}</h1>
<div>{{postt.timestamp}}</div>
<div>{{postt.content}}</div>
</div>
</div>
But not for unique items like this:
<div>
<h1>{{post.title}}</h1>
<div>{{post.timestamp}}</div>
<div>{{post.content}}</div>
</div>
This is the service:
'use strict';
angular.module('app.module.blog.post')
.factory("PostService", ["$firebaseArray", "FIREBASE_URL", function($firebaseArray, FIREBASE_URL) {
var ref = new Firebase(FIREBASE_URL + "posts");
var posts = $firebaseArray(ref);
return {
all: posts, // ng-repeat on this works fine
last: function(nr) {
var query = ref.orderByChild("timestamp").limitToLast(nr);
return $firebaseArray(query); // ng-repeat on this work fine to
},
create: function (post) {
return posts.$add(post);
},
get: function (postId) {
console.log(postId); // This is -JkZwz-tyYoRLoRqlI_I
var post = posts.$getRecord(postId);
console.log(post); // This print null
return post;
},
delete: function (post) {
return posts.$remove(post);
}
};
}]);
As the comments say in the get function, the postId is there and posts is also set, but the post is null.
This is the controller
'use strict';
angular.module('app.module.blog.post', [])
.controller('PostCtrl', ['$scope', '$routeParams', 'PostService', function($scope, $routeParams, PostService) {
// This returns e.g. postId "-JkZwz-tyYoRLoRqlI_I"
console.log($routeParams.postId);
$scope.post = PostService.get($routeParams.postId);
$scope.posts = PostService.all; // Illustrates the example not actually in this controller otherwise
}]);
This is what is an example on what is in the firebase database
<myfirebase>
posts
-JkUnVsGnCqbAxbMailo
comments
content: ...
timestamp: ...
title: ...
-JkZwz-tyYoRLoRqlI_I
comments
content: ...
timestamp: ...
title: ...
-JkhaEf9tQy06cOF03Ts
content: ...
timestamp: ...
title: ...
I find this problem very wierd since it should be very standard. I am obviously missing something, but can't work it out. Any help is very much appreciated!
Thanks in advance!
I know that the documentation of the $getRecord() function is kind of misleading. What you actually get from $firebaseArray is a promise of an array. It means that your posts variable will contain your posts at some point in the future. That being said, it seems that the $getRecord function only works when the promise have been resolved, i.e. when the array has been downloaded from Firebase. To make sure that the promise is resolved when you call the $getRecord function, you can use $loaded() on the promise :
var posts = $firebaseArray(ref);
posts.$loaded().then(function(x) {
var post = x.$getRecord(postId);
console.log(post);
}).catch(function(error) {
console.log("Error:", error);
});
If you are wondering why it works for ng-repeat, it's because Angular knows that the posts variable is a promise and waits for it to be resolved before rendering the values.
This is happening due to promises.
Along the lines of what Kato, Jean-Philippe said, $firebaseArray is not immediately available as it needs to be downloaded.
See the .$loaded() documentation:
.$loaded() "returns a promise which is resolved when the initial array data has been downloaded from Firebase. The promise resolves to the $firebaseArray itself."
That answers your question, and I just wanted to show another way of doing it:
This is a great use case for extending AngularFire services.
As the AngularFire API Documentation says:
"There are several powerful techniques for transforming the data downloaded and saved by $firebaseArray and $firebaseObject. These techniques should only be attempted by advanced Angular users who know their way around the code."
Putting all that together, you accomplish what you want to do by:
Extending the Firebase service $firebaseArray
Following the documentation for extending services.
Example
Here is a working JSFIDDLE example I put together that is tied to one of my public Firebase instances.
It's important to note that you should add ".indexOn":"timestamp" to your rules for /posts.
Factories
app.factory('PostsArray', function (FBURL, PostsArrayFactory) {
return function (limitToLast) {
if (!limitToLast) {
console.error("Need limitToLast");
return null;
}
var postsRef = new Firebase(FBURL + '/posts').orderByChild('timestamp').limitToLast(limitToLast);
return new PostsArrayFactory(postsRef);
}
});
app.factory('PostsArrayFactory', function ($q, $firebaseArray) {
return $firebaseArray.$extend({
getPost: function (postKey) {
var deferred = $q.defer();
var post = this.$getRecord(postKey);
if (post) {
console.log("Got post", post);
deferred.resolve(post);
} else {
deferred.reject("Post with key:" + postKey + " not found.");
}
return deferred.promise;
},
createPost: function (post) {
var deferred = $q.defer();
post.timestamp = Firebase.ServerValue.TIMESTAMP;
this.$add(post).then(function (ref) {
var id = ref.key();
console.log("added post with id", id, "post:", post);
deferred.resolve(ref);
}).
catch (function (error) {
deferred.reject(error);
});
return deferred.promise;
}
});
});
Controller
app.controller("SampleController", function ($scope, PostsArray) {
var posts = new PostsArray(5);
$scope.posts = posts;
$scope.newPost = {};
$scope.createNewPost = function () {
posts.createPost($scope.newPost);
}
$scope.postId = '';
$scope.getPost = function () {
posts.getPost($scope.postId).then(function (post) {
$scope.gotPost = post;
}).
catch (function (error) {
$scope.gotPost = error;
});
}
});
I have the following controller code:
.controller('Controller1', function ($scope, MyService) {
var promise = MyService.getData();
promise.then(function(success) {
console.log("success");
}, function(error) {
console.log("error");
}, function(update) {
console.log("got an update!");
}) ;
}
And in my services.js:
.factory('MyService', function ($resource, API_END_POINT, localStorageService, $q) {
return {
getData: function() {
var resource = $resource(API_END_POINT + '/data', {
query: { method: 'GET', isArray: true }
});
var deferred = $q.defer();
var response = localStorageService.get("data");
console.log("from local storage: "+JSON.stringify(response));
deferred.notify(response);
resource.query(function (success) {
console.log("success querying RESTful resource")
localStorageService.add("data", success);
deferred.resolve(success);
}, function(error) {
console.log("error occurred");
deferred.reject(response);
});
return deferred.promise;
}
}
})
But for some reason the deferred.notify call never seems to execute and be received within the controller. Have I don't something wrong here? I'm not sure how to get the notify to execute.
I managed to get it working by wrapping notify in $timeout function:
$timeout(function() {
deferred.notify('In progress')
}, 0)
Looks like you cant call notify before you return promise object, that kinda makes sense.
source: http://www.bennadel.com/blog/2800-forcing-q-notify-to-execute-with-a-no-op-in-angularjs.htm
Forcing $q .notify() To Execute
The beauty of the .notify() event is that our data-access layer can use it serve up the "immediately available, yet stale" data while still using the .resolve() event for nothing but live data. This gives the calling context - your controller - great insight and control over which dataset is cached and whether or not it [the controller] even wants to incorporate cached data.
But, we run into a little bit of a race condition. The data-access service, that owns the cached data, needs to call .notify() before it returns the promise to the calling context. This means that your controller binds to the notify event after .notify() has been called. From a philosophical standpoint, this should be fine - Promises (and just about everything that is event-driven) are intended to invoke bindings asynchronously in order to create uniformity of access.
From a practical standpoint, however, it's not quite that simple. While AngularJS follows this philosophy, it also adds a few optimizations to cut down on processing. In our case specifically, AngularJS won't schedule the callback-processing in a deferred object unless it sees that at least one callback is bound (otherwise it thinks the world isn't listening). As such, our controller will never be notified about the cached data.
To get around this, we can have our service layer bind a no-op (no operation) function to the notify event before it calls .notify(). This way, when it does call .notify(), AngularJS will see that at least one callback is registered and it will scheduled a flushing of the pending queue in the next tick (which is implemented via $rootScope.$evalAsync()). This allows our controller to get notified of cached data even if it binds to the notify event after .notify() has been invoked.
To see this in action, I've created a friendService that returns data through two different methods. Both of the methods attempt to return cached data via .notify() and then "live" data via .resolve(). The only difference between the two methods is that one binds a no-op to the notify event before calling .notify()
<!doctype html>
<html ng-app="Demo">
<head>
<meta charset="utf-8" />
<title>
Forcing $q .notify() To Execute With A No-Op In AngularJS
</title>
<link rel="stylesheet" type="text/css" href="./demo.css"></link>
</head>
<body ng-controller="AppController">
<h1>
Forcing $q .notify() To Execute With A No-Op In AngularJS
</h1>
<h2>
Friends
</h2>
<div ng-switch="isLoading">
<!-- Show while friends are being loaded. -->
<p ng-switch-when="true">
<em>Loading...</em>
</p>
<!-- Show once the friends have loaded and are available in the view-model. -->
<ul ng-switch-when="false">
<li ng-repeat="friend in friends track by friend.id">
{{ friend.name }}
</li>
</ul>
</div>
<p>
<a ng-click="load()">Load</a>
|
<a ng-click="loadWithNoop()">Load With No-Op</a>
</p>
<!-- Load scripts. -->
<script type="text/javascript" src="../../vendor/angularjs/angular-1.3.13.min.js"></script>
<script type="text/javascript">
// Create an application module for our demo.
var app = angular.module( "Demo", [] );
// -------------------------------------------------- //
// -------------------------------------------------- //
// I control the root of the application.
app.controller(
"AppController",
function( $scope, friendService ) {
$scope.isLoading = false;
$scope.friends = [];
// Load the friend data (defaults to "get" method vs. "getWithNoop").
loadRemoteData();
// ---
// PUBLIC METHODS.
// ---
// I reload the list of friends using friendService.get().
$scope.load = function() {
loadRemoteData( "get" );
};
// I reload the list of friends using friendService.getWithNoop().
$scope.loadWithNoop = function() {
loadRemoteData( "getWithNoop" );
};
// ---
// PRIVATE METHODS.
// ---
// I load the friends from the friend repository. I am passing-in the
// method name to demonstrate that, from the Controller's point-of-view,
// nothing here is different other than the name of the method. The real
// substantive difference exists in the implementation of the friend-
// Service method and how it interacts with $q / Deferred.
function loadRemoteData( loadingMethod ) {
console.info( "Loading friends with [", loadingMethod, "]" );
// Indicate that we are in the loading phase.
$scope.isLoading = true;
// When we make the request, we expect the service to try to use
// cached-data, which it will make available via the "notify" event
// handler on the promise. As such, we're going to wire up the same
// event handler to both the "resolve" and the "notify" callbacks.
friendService[ loadingMethod || "get" ]
.call( friendService )
.then(
handleResolve, // Resolve.
null,
handleResolve // Notify.
)
;
function handleResolve( friends ) {
// Indicate that the data is no longer being loaded.
$scope.isLoading = false;
$scope.friends = friends;
console.log( "Friends loaded successfully at", ( new Date() ).getTime() );
}
}
}
);
// -------------------------------------------------- //
// -------------------------------------------------- //
// I provide access to the friend repository.
app.factory(
"friendService",
function( $q, $timeout ) {
// Our friend "repository".
var friends = [
{
id: 1,
name: "Tricia"
},
{
id: 2,
name: "Heather"
},
{
id: 3,
name: "Kim"
}
];
// Return the public API.
return({
get: get,
getWithNoop: getWithNoop
});
// ---
// PUBLIC METHODS.
// ---
// I return the list of friends. If the friends are cached locally, the
// cached collection will be exposed via the promise' .notify() event.
function get() {
var deferred = $q.defer();
// Notify the calling context with the cached data.
deferred.notify( angular.copy( friends ) );
$timeout(
function networkLatency() {
deferred.resolve( angular.copy( friends ) );
},
1000,
false // No need to trigger digest - $q will do that already.
);
return( deferred.promise );
}
// I return the list of friends. If the friends are cached locally, the
// cached collection will be exposed via the promise' .notify() event.
function getWithNoop() {
var deferred = $q.defer();
// -- BEGIN: Hack. ----------------------------------------------- //
// CAUTION: This is a work-around for an optimization in the way
// AngularJS implemented $q. When we go to invoke .notify(),
// AngularJS will ignore the event if there are no pending callbacks
// for the event. Since our calling context can't bind to .notify()
// until after we invoke .notify() here (and return the promise),
// AngularJS will ignore it. However, if we bind a No-Op (no
// operation) function to the .notify() event, AngularJS will
// schedule a flushing of the deferred queue in the "next tick,"
// which will give the calling context time to bind to .notify().
deferred.promise.then( null, null, angular.noop );
// -- END: Hack. ------------------------------------------------- //
// Notify the calling context with the cached data.
deferred.notify( angular.copy( friends ) );
$timeout(
function networkLatency() {
deferred.resolve( angular.copy( friends ) );
},
1000,
false // No need to trigger digest - $q will do that already.
);
return( deferred.promise );
}
}
);
</script>
</body>
</html>
As you can see, the controller binds the same handler to the "resolve" and "notify" event of the promise. In this way, it can handle the cached data and the live data uniformly. The only difference is in which service-layer method it invokes - get() vs. getWithNoop(). And, if we invoke .get() a few times and then .getWithNoop() a few times, we can see the difference in the console.
I tried to reproduce your problem here. It seems, that you cannot call notifyon the promise directly, but have to wrap into an $applycall.
See also the documentation for $q here.
To quote the exact lines from the example:
since this fn executes async in a future turn of the event loop, we need to wrap our code into an $apply call so that the model changes are properly observed.
You could try this your self and change your code a little bit:
deferred.notify(response); // should not work
resource.query(function (success) {
deferred.notify('Returning from resource'); // should work
console.log("success querying RESTful resource")
localStorageService.add("data", success);
deferred.resolve(success);
}, function(error) {
deferred.notify('caught error!'); //should also work
console.log("error occurred");
deferred.reject(response);
});
I have a 'messages' factory that will query my database for a list of messages.
I'm using the list of messages in two different places. Once to add a message count indicator, and then once to show a list of messages. Since I'm injecting the service into two different controllers, it seems like it's creating two instances of my factory, and hitting the database twice for the list.
How would I set things up to only ask for the list once, and use the list for both display and count purposes in both controllers?
My factory looks like this:
myApp.factory('messagesService', [
'$rootScope',
function($rootScope) {
var messages = [];
function query() {
// Would actually hit the database asyncronously
messages = ['one', 'two', 'three', 'four'];
console.log('query');
$rootScope.$emit('messages.update');
}
function all() {
return messages;
}
return {
query: query,
all: all
}
}
]);
My controllers are using blocks like this to watch for changes:
$rootScope.$on('messages.update', function() {
$scope.messagesCount = messagesService.all().length;
});
But it means i need a messagesService.query(); in each controller for things to be reliable.
So here are a few jsFiddle examples of it as I have things now:
Doesn't work (only updates the header): http://jsfiddle.net/TSLfc/1/
Works but would break if I didn't load the dashboard controller:
http://jsfiddle.net/TSLfc/2/
Works every time, but queries the server twice:
http://jsfiddle.net/TSLfc/3/
Is there a better way to organize my code? Should I build out the messages factory into it's own full module?
Here (Plunkr) is how I would do it:
I have gone back and modified my previous answer, updating with what we discussed in the comments below as well as using promises instead of the timeout as an asynchronous simulation I was showing before (see revision history for reference).
I also removed every variable/function that didn't need to be returned to the controller from the service object, if it doesn't need to be accessed via the controller than it doesn't need to be included on the returned object.
var myApp = angular.module('myApp', []);
myApp.factory('messagesService', [
'$q',
'$rootScope',
'$http',
function ($q, $rootScope, $http) {
var mService = {};
mService.messages = [];
var queryInit = false;
// We don't need to access this function in the controller
// So I am not going to attach to the returned object
var getMessages = function () {
// Stops each controller from getting messages when loaded
if (!queryInit) {
queryInit = true;
// Using the $q promise library we use 'then()' to handle
// What happens after the async call is returned
// The first function parameter is the success/resolve callback
// The second function parameter is the error/reject callback
mService.query().then(function (successResults) {
// Tell all of the controllers that the data has changed
$rootScope.$broadcast('messages.update');
}, function (errorResults) {
console.error(errorResults);
});
}
};
// Used to force an update from the controller if needed.
mService.query = function () {
var deferred = $q.defer();
$http.get('path/to/file.php')
.success(function (data, status, headers, config) {
// assign the returned values appropriately
mService.messages = data;
// this callback will be called asynchronously
// when the response is available
deferred.resolve(data);
})
.error(function (data, status, headers, config) {
// called asynchronously if an error occurs
// or server returns response with an error status.
deferred.reject(data);
});
return deferred.promise;
};
mService.getCount = function () {
return mService.messages.length;
};
mService.all = function () {
return mService.messages;
};
// Initialize the messages
// so we don't need to get the messages in each controller
getMessages();
return mService;
}]);
In your html, on your first controller setup an init function (ng-init="init()") that instantiates the factory:
<div ng-app="myApp">
<div ng-controller="HeaderCtrl" class="header" ng-init="init()">
Messages Count: {{ messageCount }}
</div>
<div ng-controller="DashboardCtrl" class="dashboard">
<ul ng-repeat="message in messages">
<li>{{ message }}</li>
</ul>
<button ng-click="getMessages()">Check for new messages.</button>
</div>
</div>
And in your controllers you just have the $rootScope.$on('messages.update' fn) and you can call manually by calling the services query() function which returns the promise:
myApp.controller('HeaderCtrl', [
'$scope',
'$rootScope',
'messagesService',
function ($scope, $rootScope, messagesService) {
$rootScope.$on('messages.update', function () {
$scope.messageCount = messagesService.getCount();
});
// Manual call, if needed
$scope.getMessageCount = function () {
messagesService.query().then(function (successCallback) {
$scope.messageCount = messagesService.getCount();
});
};
}]);
myApp.controller('DashboardCtrl', [
'$scope',
'$rootScope',
'messagesService',
function ($scope, $rootScope, messagesService) {
$rootScope.$on('messages.update', function () {
$scope.messages = messagesService.all();
});
// Manual call, if needed
$scope.getMessages = function () {
messagesService.query().then(function (successCallback) {
$scope.messages = messagesService.all();
$rootScope.$broadcast('messages.update');
});
}
}]);
You can set cache:true on a $http request. There are numerous ways to data bind within angular without needing to use the $broadcast approach you are using. Also note, $broadcast from a scope will be receievd by all descendent scopes, so no need to inject $rootSCope just for that purpose, can listen on $scope.
Here's one approach that controllers use promise of $http to retrieve data. I used a button click to retrive data for DashControl so can see that request does get cached
myApp.factory('messagesService',function($http) {
return{
query:function query(callback) {
/* return promise of the request*/
return $http.get('messages.json',{ cache:true}).then(function(res){
/* resolve what data to return, can set additional properties of the service here if desired*/
return res.data
}).then(callback);
}
}
});
myApp.controller('HeaderCtrl',function($scope, messagesService) {
messagesService.query(function(messages){
$scope.messagesCount = messages.length;
});
});
myApp.controller('DashboardCtrl', function($scope, messagesService) {
/* use button click to load same data, note in console no http request made*/
$scope.getMessages=function(){
messagesService.query(function(messages){
$scope.messages = messages;
})
}
});
Essentially in this scenario, whatever controller calls the factory service first will generate the data cache
DEMO
I would do it like that:
myApp.factory('messagesService', function() {
var expose = {
messages: []
};
expose.query = function () {
// Would actually hit the database asyncronously
expose.messages = ['one', 'two', 'three', 'four'];
console.log('query');
};
// Initialization
expose.query();
return expose;
}
);
And in your controllers:
$scope.messagesCount = messagesService.messages.length;
Model with broadcasting and pre-hitting database looks heavy for me.
So here is code, that can be embedded in service:
var sv = this;
var deferred = sv.$q.defer();
if (sv._running) {
return sv._running;
}
sv._running = deferred;
It based on reusing promise. To make it query database once - just don't set sv._running to false and it will always return first obtained result.