Check for any ongoing background API calls to finish in AngularJS - angularjs

My AngularJS controller is calling an API to get a list of members. The API can be queried with multiple filters, like age range, name search, zipcode, etc.
What I am trying to accomplish is to make the user feel like the search is really fast. So while the user is filtering, I am $watch'ing the filters for changes. When a filter changes I immediately do a new search API call, instead of waiting for the user to click "search".
The idea is that when the user finishes filtering and clicks "search", the system should already be finished searching and can just output the search results.
The problem: When a user clicks "search" and the publish() function is called, I have to make sure that no API search is currently ongoing and if one is, wait for it to finish. I then publish the search results from the hidden memberslist.temp to the visible memberslist.members.
// Current code works, except the publish function. See the TODO:
app.controller('SearchCtrl', ['$scope', 'Restangular', '$timeout', function($scope, Restangular, $timeout) {
// Pushes prefetched search results over to the visible search results
// TODO: Check if search is ongoing...
$scope.publish = function() {
$scope.memberslist.members = $scope.memberslist.temp;
};
// Watch filters for changes
var searchTimer = false;
var previousSearch = [$scope.name, $scope.minAge, $scope.maxAge];
$scope.$watch('[name, minAge, maxAge]', function(newVal, oldVal, scope){
if (searchTimer) {
$timeout.cancel(searchTimer);
}
// Do search only after the user has stopped typing for 400 ms
searchTimer = $timeout(function(){
if (!_.isEqual(newVal, previousSearch)) {
previousSearch = newVal;
scope.search();
}
}, 400);
}, true);
$scope.search = function() {
var query_params = {
name: $scope.name,
price_min: $scope.minAge,
price_max: $scope.maxAge
};
Restangular.all('members').getList(query_params)
.then(function(members){
$scope.memberslist.temp = members;
});
};
}]);
My first thought was to set a variable like isSearching = true, then set that back to false once the search call returns it's results. Can't seem to get it to work though.

Perhaps something like:
$scope.$watch('!is_searching && publishing', function(newVal){
if (newVal) {
$scope.memberslist.members = $scope.memberslist.temp;
$scope.publishing = false;
}
})
Set "is_searching" to true on the scope when the search starts, and set it to false when it is done. Set "publishing" on the scope when the search button is clicked.
That should give you the desired behavior. Publish right away if no search is running, otherwise wait until search is complete.

Related

Angular form $invalid and $routeUpdate

I am implementing an advanced search in an application I'm working on. In this search I want the back and next buttons from the browser to work with the search.
To achieve this I added this to my $routeProvider:
.when('/search', {
templateUrl: 'Scripts/App/Views/Search.html',
controller: 'SearchController',
reloadOnSearch: false
})
In my SearchController I have a search function on the $scope that just adds some query string parameters to my url:
$scope.advancedSearch = function(){
$location.search("page", $scope.currentPage);
$location.search("groupingField", $scope.groupingField);
$location.search("topgroup", angular.toJson($scope.filter));
}
I listen to $routeUpdate, get the query string parameters and execute the search with these parameters.
$scope.$on('$routeUpdate', function () {
var qs = $location.search();
var topGroup = qs["topgroup"];
$scope.filter = angular.fromJson(topGroup);
$scope.groupingField = qs["groupingField"];
$scope.currentPage = qs["page"];
performSearch();
});
In my real search method I check the form for errors. If there are any I show a message else I perform the search (do an api call).
var performSearch = function () {
if ($scope.searchForm.$invalid) {
$scope.message = "Please fix the errors before you search.";
} else {
//get search results from database and put them on $scope.
}
}
With this code I can search and get the correct results. If I do a new search it also get's the correct results and I can use the back and next buttons from the browser.
When I do a search and my form is invalid the message shows, but when I go back after an invalid search the $invalid of the form updates after my "performSearch" method is called. This causes "Please fix the errors before you search." message to display even if the form is $valid.
If I click on the next button after this I get even more trouble since the form is $valid now but with the querystring parameters filled in it should be $invalid. Again this only updates after the performSearch has been called. wich is to late.
It might be hard to understand my problem, if something is unclear ask away!
Found the answer!
On the $routeupdate I waited for a digest cycle to finish and update the form properties. This can be done with a $timeout without a time.
$scope.$on('$routeUpdate', function () {
var qs = $location.search();
var topGroup = qs["topgroup"];
$scope.filter = angular.fromJson(topGroup);
$scope.groupingField = qs["groupingField"];
$scope.currentPage = qs["page"];
$timeout(function () {
performSearch();
});
});

Angular using Server Sent Events in a factory

I am trying to setup message notifications in an Angular/Rails app.
When the user logs in, I want to open a SSE connection which will subscribe to a Redis stream and push an alert to the client when the user gets a new message.
I have my SSE setup and working, but cannot figure out a way to reliably close the SSE connection when a user logs out. I am trying to build a service to handle SSE opening and closing:
angular.module('messagesApp')
.factory('StreamHandler', function(CookieHandler, MessageStream){
var StreamHandler = {
set: function(){
var user
user = CookieHandler.get();
MessageStream.get = function(){
var source = new EventSource('/api/v1/messages/count?id='+user.id)
return source
}
},
get: function(){
var source = MessageStream.get()
source.onmessage = function(event) {
//do something
}
},
kill: function(){
var source = MessageStream.get()
source.close()
}
}
return StreamHandler
})
I cannot figure out how to kill the stream that is opened in StreamHandler.set(). My attempt in the kill attribute does not work, possible because calling the getter actually creates a new stream?
I am open to other approaches: I just need a way to set and kill an EventSource stream on user login/logout.
The problem was that I was putting the function that creates an EventSource in my get attribute, instead of putting the actual EventSource object. A few changes makes it work:
.factory('StreamHandler', function(CookieHandler, MessageStream){
var StreamHandler = {
set: function(){
var user
user = CookieHandler.get();
var source = new EventSource('/api/v1/messages/count?id='+user.id)
MessageStream.get = source
},
get: function(){
var source = MessageStream.get
source.onmessage = function(event) {
console.log(event)
}
source.onerror = function(error) {
source.close()
}
},
kill: function(){
var source = MessageStream.get
source.close();
}
}
return StreamHandler
})
Look into Oboe.js - http://oboejs.com/examples
Using Oboe, I basically did (and I guess you don't need to inject $source and $http either in this case):
.factory('MyStreamingResource', ['$resource', '$http',
function($resource, $http) {
return {
stream: function(options, startFn, nodeFn, doneFn) {
oboe('//url/' + 'maybeSomeOptions/?maybe=' + options.passedAbove)
.start(startFn)
.node(options.path, nodeFn)
.done(doneFn);
}
};
}
]);
Then simply injected it and called from some controllers with:
MyStreamingResource.stream({
passedAbove: 'foo',
path: 'items.*'
},
// start callback
function(status, headers){
// console.dir(headers);
// this.abort(); // could be called from here too
},
// node callback (where your data is going to be streamed to)
function(data){
if(data !== null) {
console.dir(data);
//this.abort();
}
},
// done (if you really want to wait)
function(parsedJson){
// ...
});
Very similar to other services that you'd see with $http, but instead you have a few more callbacks to consider.
Oboe.js worked like a charm for me and the Golang go-json-rest package streaming a response (even with invalid JSON - which is quite common with streams since they aren't complete and won't have closing tags or will have extra commas, etc.). Just ensure the browser version of the script gets included on your page and you can just call it where ever. I spent a good while searching on how to do this with AngularJS, but there just didn't seem to be a straight forward facility for it. If it is at all possible.
Of course from here you could probably figure out how to make it work for your needs...Update something in the $scope, etc.

watchPosition not working correctly by using ngroute (Timeout, code 3)

I use navigator.geolocation.watchPosition to find out the current location insight an angularJS application. That works well.
Now i added the ngRoute functionality to the page.
Starting the application redirects the user to #/main where a controller gets the current position an displays this position on an map.
That still works.
This is the controller:
MyMapCtrl.controller('watchPosMap', ['$scope', 'MarkerService', function($scope) {
$scope.ha = true;
$scope.timeout = 20000;
$scope.processNewLocation = function (data) {
[..code to process an display the location ...]
};
$scope.watchMyPositionChangesID =
navigator.geolocation
.watchPosition(
function(data) {
$scope.processNewLocation(data);
},
function(e){
$scope.errorMsg = e;
if (e.code == 2)
{alert('Unable to detect location');}
},
{ enableHighAccuracy: true,timeout: $scope.timeout });
}]);
The Problem is: if the application goes to an different route (like from #/main to #/detail) an than back to the main route, the map is displayed, but the geolocation seams not to work.
The line $scope.processNewLocation(data) is not executed anymore.
While debugging that situation I found out, that $scope.watchMyPositionChangesID is lost after returning to #/main, navigator.geolocation is called again, but results in an timeout. So $scope.processNewLocation(data); is not called again, the watchPositionresults in timeout (Code 3)
http://dev.w3.org/geo/api/spec-source.html#position_error_interface
So no new position-object could successfully acquired.
I have no idea way?!
//Update:
It seams to work, if I remove the "geolocation-watch-objekt" by using navigator.geolocation.clearWatch(id);.
The problem is: how to determine the id outside the watchPosMap-controller? I think fetching the angular-element is not an elegant idea, right? Should I pass the watchID as an parameter to the new route/view/controller?
//Update 2:
I added to the watchPosMap - controller an $routeChangeStart listener in order to remove the watchPosition object, if the route changed.
$scope.$on('$routeChangeStart', function(next, current) {
navigator.geolocation.clearWatch($scope.watchMyPositionChangesID);
});
Is that best practice?

Is this the best way to retrieve an array with $firebase()?

I have arrays stored in Firebase, one of which I need to retrieve when a user logs in. Each user has their own array which requires authentication for read. (It would be inconvenient to switch to another data structure). Since $firebase() always returns an object, as per the docs, I'm using the orderByPriority filter. However, if I do simply
$scope.songs = $filter('orderByPriority')($firebase(myref));
that doesn't work as songs always get an empty array.
I don't understand why this happens, but what I've done to solve it is use the $firebase().$on('loaded',cb) form and applied the filter in the callback. Is this a good solution?
The drawback is that I cannot do $scope.songs.$save()
Here's my controller, including this solution:
.controller('songListController', function($scope, $rootScope, $firebase, $filter, $firebaseSimpleLogin){
var authRef = new Firebase('https://my-firebase.firebaseio.com/users'),
dataRef;
$scope.loginObj = $firebaseSimpleLogin(authRef);
$scope.songs = [];
$rootScope.$on("$firebaseSimpleLogin:login", function(event, user) {
// user authenticated with Firebase
dataRef = $firebase(authRef.child(user.id));
dataRef.$on('loaded', function(data){
$scope.songs = $filter('orderByPriority')(data);
});
});
//other controller methods go here
$scope.save = function(){
if (!$scope.loginObj.user)
{
alert('not logged in. login or join.');
return;
}
//Was hoping to do this
//$scope.songs.$save().then(function(error) {
//but having to do this instead:
dataRef.$set($scope.songs).then(function(error) {
if (error) {
alert('Data could not be saved.' + error);
} else {
alert('Data saved successfully.');
}
});
};
});
---Edit in response to Kato's answer---
This part of my app uses Firebase as a simple CRUD json store without any realtime aspects. I use $set to store changes, so I think I'm okay to use arrays. (I'm using jQueryUI's Sortable so that an HTML UL can be re-ordered with drag and drop, which seems to need an array).
I don't need realtime synchronisation with the server for this part of the app. I have a save button, which triggers the use of the $scope.save method above.
The problem with the approach above is that orderByPriority makes a single copy of the data. It's empty because $firebase hasn't finished retrieving results from the server yet.
If you were to wait for the loaded event, it would contain data:
var data = $firebase(myref);
data.$on('loaded', function() {
$scope.songs = $filter('orderByPriority')(data);
});
However, it's still not going to be synchronized. You'll need to watch for changes and update it after each change event (this happens automagically when you use orderByPriority as part of the DOM/view).
var data = $firebase(myref);
data.$on('change', function() {
$scope.songs = $filter('orderByPriority')(data);
});
Note that the 0.8 release will have a $asArray() which will work closer to what you want here. Additionally, you should avoid arrays most of the time.

AngularJS model updates but view doesn't when using $cookieStore

I have a search 'page' in my AngularJS app which essentially consists of a view that holds the search form (and displays the results) and a controller which handles the search request. When the user types in a search query and hits 'Search', the $scope.submit() method is called and I can see the results properly. However, when the user clicks on a result and then goes back to the search page, it is blank. I thought of implementing a $cookieStore-based solution so that the query is stored in a cookie and whenever the user goes back to the search page, it automatically re-runs the previous search so they don't have to do it manually. Problem is, the model updates (search gets run from cookieStore value) but the view stays the same (blank). Here's a sample of my controller:
.controller('SearchCtrl', ['$scope', '$http', '$cookieStore','authService', function($scope, $http, $cookieStore, authService) {
var submitted = false;
$scope.submit = function(query){
$cookieStore.query = query;
submitted = true;
$http.jsonp(url).success(function(data) {
$scope.searchResults = data;
});
}
/*
Rerun query if user has pressed "back" or "home" button automatically:
*/
if(!submitted && $cookieStore.query){
console.log("submitting query from cookie store", $cookieStore.query);
$scope.submit($cookieStore.query);
}
... });
I tried using $scope.$apply() after the auto-search but still no joy. The view just won't update. Any hints you guys could give me? Cheers
You should place $scope.$apply at the end of your callback function. It's because $http makes an async AJAX call and by the time response comes back Angular is already done auto-$applying changes. So when you check the model you see the difference but since Angular is no longer $applying the difference cannot be seen on the view.
So when you add $scope.$apply you will have something like this:
.controller('SearchCtrl', ['$scope', '$http', '$cookieStore','authService', function($scope, $http, $cookieStore, authService) {
var submitted = false;
$scope.submit = function(query){
$cookieStore.query = query;
submitted = true;
$http.jsonp(url).success(function(data) {
$scope.searchResults = data;
$scope.$apply();
});
}
/*
Rerun query if user has pressed "back" or "home" button automatically:
*/
if(!submitted && $cookieStore.query){
console.log("submitting query from cookie store", $cookieStore.query);
$scope.submit($cookieStore.query);
}
... });

Resources