AngularJS - Handling dynamic search using $http and debounce - javascript

I have a text input below, bound to a model req.mod1, with a debounce delay in updating model, that calls a pullData() function.
<input type="text" class="form-control" ng-change="pullData()" ng-model-options="{debounce: 1000}" ng-model="req.mod1">
Inside of pullData() I have a simple $http.get request, that pulls data and updates some other fields in the form.
$scope.pullData = function(){
var pullingData = true;
if (!pullingData){
$http.get('example.com/search/' + $scope.req.mod1 ).then(res)
...
$scope.req.mod2 = res.data.mod2;
$scope.req.mod3 = res.data.mod3;
var pullingData = false;
...
}
}
The problem that arises is that multiple calls stack on top of each other -- I think -- and so if a user happens to input text, wait >1second, and type some more, the call would go out with the first inputted text. Then it would pull the data and update the form with the first res. I'm trying to keep track of the request with a pullingData var.
Any ideas on how to handle a truly dynamic search call? Is there a way to cancel requests if a new one comes in? Or maybe just tell angular to constantly overwrite it?
Thanks in advance.

I think this is what you need
http://odetocode.com/blogs/scott/archive/2014/04/24/canceling-http-requests-in-angularjs.aspx
When you create a request.. it's called Promise, so what you need to cancel is that.
Something like this:
app.factory("movies", function($http, $q){
var getById = function(id){
var canceller = $q.defer();
var cancel = function(reason){
canceller.resolve(reason);
};
var promise = $http.get("/api/movies/slow/" + id, { timeout: canceller.promise})
.then(function(response){
return response.data;
});
return {
promise: promise,
cancel: cancel
};
};
return {
getById: getById
};
});

When the user enters something to search, your app should always search by the user's input. It means that you shouldn't cancel the user's input.
For example, the user want to search something about 'starwar', when he/she enter 'star', and you get some result. If now you cancel the 'war' which is entered after, the result is not good. So just let Angular override the result.
Moreover, some errors in your example of code, when you call pullData, it never passes the if check:
$scope.pullData = function(){
var pullingData = true;
// !pullingData is always false
if (!pullingData){
$http.get('example.com/search/' + $scope.req.mod1 ).then(res)
}
}

Related

Use angular-ui bootstrap typeahead with api callback function

I'm trying to add a typeahead control to my project. I tried using the filter option, but sometimes, results are in the range of the thousands, which bring my app's performance to a crawl
So i'm going for the asynchronous method.
The problem here is, I can't use $http service to consume the api, I need to use the customer proprietary method, due to authorization reasons and it doesn't return a promise, I need to pass a success and fail callback function in order to get the results.
This is the structure of my typeahead
<div class="input-group typeahead">
<input type="text" class="form-control" ng-model="stopToEdit.STOP_SITE" uib-typeahead="site.SITE_CODE as site.SITE_NAME for site in sites" ng-keyup="fnGetSites(stopToEdit.STOP_SITE, stopToEdit.STOP_TYPE)"
typeahead-loading="loadingLocations" typeahead-no-results="noResults" typeahead-popup-template-url="siteList.html" />
<span class="input-group-btn">
<button class="btn btn-default" type="button" ng-click="fnSearchSites()"><span class="fa fa-binoculars"></span></button>
</span>
</div>
And this is the method I call in order to fill the sites array
$scope.fnGetSites = function (val, stopType) {
if (val === '') {
$scope.sites = null;
return;
} else if (val.length < 4) {
$scope.sites = null;
return;
}
$scope.showLoadingSpinner = true;
blockUIOnCall = false;
customerPropietaryObject.get(<api rest url to consume>, function (results) {
$scope.sites = results;
$scope.$digest();
}, $scope.fnShowErroMsg);
}
The problem is, results are stored correctly, but neither $digest nor $apply are working in order to show the results at the moment. I need to make another action, like, writing another character in the text box or activating the field blur in order to show the results.
So, if I write 'stev', I do get the results, but until I write 'steve' I get the results for 'stev'
Any idea how to solve and accomplish this?
Thanks
Well, after days of leaving this behind and focusing on something else, I finally decided to try and solve it.
The solution, in the end, was quite simple, I just needed to create my own promise as a starting point and convert my client proprietary method into a promise, and ended up with
var prom = new Promise(function (resolve, reject) {
customerPropietaryObject.get(<api rest url to consume>, function (results) { resolve(results) }, function (data) { reject(data) });
});
return prom.then(function (results) {
return results;
});
Now, I just need to figure out how to show the data as a list of KEY | VALUE as query of the autocomplete is either for that the key or the value contains the text field text

Angularjs:Empty object, when have only one click?

This function i call on ng-click.
HTML
<div class="col-md-offset-10">
<div class="form-group">
<a class="btn btn-default" ng-click="schedule ()">Add</a>
</div>
</div>
JS
$scope.scheduleid={};
var inc=0;
$scope.schedule = function ()
{
inc += 1;
console.log($scope.from);
console.log ($scope.dateObj);
if (inc == 1)
{
var schedule = {}
schedule._type = "Schedule";
console.log($scope.dateObj);
var insertSchedule = BasicJSONCreator.basicEdit ();
insertSchedule.Calls[0].Arguments = [];
insertSchedule.Calls[0].Arguments[0] = schedule;
httpRequester.async (insertSchedule).then (function (data) {
console.log(data.data.ReturnValues[0].scheduleID);
$scope.scheduleid=data.data.ReturnValues[0] //This get response from my database
console.log($scope.scheduleid ); // here object is get my data from database
});
}
$scope.getData();
}
This funciton i want get result from $scope.scheduleid, but here is problem.
When i have one click $scope.scheduleid (empty), but two and more click $scope.scheduleid (no empty and get data).Why my first click is Empty.
$scope.getData=function()
{
console.log($scope.scheduleid)
// Here my object is empty when have one click,but two and more click object no empty and get data
}
You are making an HTTP request, I think the problem is there. When you first call your function through the click, your call is being processed, and your function keeps going on.
That's why when you click once, at the end of your function, your object is empty. Once your object is loaded -let's say, at your second click-, you will finally receive it. What you should do is wait for your HTTP call to end before processing. You can do this the dirty way ($timeout) or looking at solutions like this one which should provide useful informations.

How to make html wait for a function in AngularJS controller

I have a object in mainController.js that is set as default as 99.
I am obtaining user location and do running some other function with it to calculate this value.
However, When I load the page, the page seems to load faster than this process. Therefore it displays 99 instead of the calculated value.
If I put console.log after the calculation, the object is successfully changed.
edit1:
status.success( function(data)
{
$scope.current = data;
$scope.$broadcast('back_end_connected');
});
$scope.getLocation = function()
{
if (navigator.geolocation)
{
navigator.geolocation.getCurrentPosition(function (position){
$scope.location = {lat: position.coords.latitude, lng: position.coords.longitude};
$scope.$broadcast('location_obtained');
$scope.buildDist();
$scope.fetch();
//$scope.getRec();
});
}
else{
alert("Geolocation is not supported by this browser.");
}
};
var dirBuilt = false;
$scope.$on('location_obtained', function(){
$scope.buildDist = function()
{
if(dirBuilt === false)
{
$scope.facilities[0].distance = distCalc($scope.location.lat,$scope.location.lng,$scope.facilities[0].location.lat,$scope.facilities[0].location.lng);
$scope.facilities[1].distance = distCalc($scope.location.lat,$scope.location.lng,$scope.facilities[1].location.lat,$scope.facilities[1].location.lng);
$scope.facilities[2].distance = distCalc($scope.location.lat,$scope.location.lng,$scope.facilities[2].location.lat,$scope.facilities[2].location.lng);
$scope.facilities[3].distance = distCalc($scope.location.lat,$scope.location.lng,$scope.facilities[3].location.lat,$scope.facilities[3].location.lng);
$scope.facilities[4].distance = distCalc($scope.location.lat,$scope.location.lng,$scope.facilities[4].location.lat,$scope.facilities[4].location.lng);
$scope.facilities[5].distance = distCalc($scope.location.lat,$scope.location.lng,$scope.facilities[5].location.lat,$scope.facilities[5].location.lng);
$scope.$broadcast('dist_obtained');
dirBuilt = true;
alert("aaa: "+ $scope.facilities[0].distance);
}
};
});
that "alert("aaa: "+ $scope.facilities[0].distance);" returns the value I want it to display but it is not displayed on the page....
(ng-bind would not work for some reason...)
How can I make the html wait for the operation? Thanks
You should not make HTML wait until you finish your Js code ! What you should be doing is showing a placeholder value - Loading image so that user know the page is loading some data.
Once you are done with your calculation, hide /replace the loading image with the data you want to show.
Quick example
Your view markup will have some HTML element to show the progress bar.And all your other contents will be in another div
<body ng-app="yourApp" ng-controller="yourCtrl as vm">
<div ng-show="loaderCount>0"> Loading something..</div>
<div ng-show="loaderCount==0">
<h4>{{userScore}}</h4>
</div>
</body>
And in your angular controller, You have a scope variable called loaderCount which you will increase everytime when you are doing some operation (http call/Long running function execution etc..). When you get your result, You decrease this variable value back. In your View You are hiding and showing the Loading Pane based on this value.
var yourApp= angular.module('yourApp', []);
var ctrl = function($scope, $http) {
var vm = this;
vm.loaderCount = 0;
vm.someValue = "";
vm.actionItems = [{ Name: "Test" }, { Name: "Test2" }];
vm.loaderCount++;
$http.get("../Home/GetSlowData").then(function(s) {
vm.loaderCount--;
vm.someValue = s.data;
});
};
yourApp.controller('yourCtrl', ['$scope', '$http', ctrl]);
This might not be the best angular code. But this will give you an idea about how to handle this use case. You should be using services to talk to Http endpoints instead of directly using $http in your angular controller.
Note that, $http.get returns a promise which allows you do things when you get the response from this asynchronous operation (the then event). You should make sure that your time taking calculation is returning a promise.
You can bind with the ngBind directive instead of {{}} and not write to the binded property when the calculation is done.
This will hide the result from the view while it is null.
Try to do something like this:
$scope.value = "Loading";
or
$scope.value = "";
$scope.calculate = function() {
$scope.value = yourcalculation;
}
$scope.calculate();
Or in your case i think if you use $scope.$apply() after you add data to your scope element will do the trick
Try this :
$scope.$on('location_obtained', function(){
$scope.buildDist = function()
{
if(dirBuilt === false)
{
$scope.facilities[0].distance = distCalc($scope.location.lat,$scope.location.lng,$scope.facilities[0].location.lat,$scope.facilities[0].location.lng);
$scope.facilities[1].distance = distCalc($scope.location.lat,$scope.location.lng,$scope.facilities[1].location.lat,$scope.facilities[1].location.lng);
$scope.facilities[2].distance = distCalc($scope.location.lat,$scope.location.lng,$scope.facilities[2].location.lat,$scope.facilities[2].location.lng);
$scope.facilities[3].distance = distCalc($scope.location.lat,$scope.location.lng,$scope.facilities[3].location.lat,$scope.facilities[3].location.lng);
$scope.facilities[4].distance = distCalc($scope.location.lat,$scope.location.lng,$scope.facilities[4].location.lat,$scope.facilities[4].location.lng);
$scope.facilities[5].distance = distCalc($scope.location.lat,$scope.location.lng,$scope.facilities[5].location.lat,$scope.facilities[5].location.lng);
$scope.$broadcast('dist_obtained');
dirBuilt = true;
alert("aaa: "+ $scope.facilities[0].distance);
$scope.$apply(); // This should do the trick
}
};
});
Are you using a promise on the service? The function running your calculation can be in a separate angular factory which has .success and .error events to tap into. If the calculation is a success. You can pass the data back to your controller and then bind that data to the controller scope. I'll be at a computer soon and will add some code to explain further if needed.
This would be your distance calculator, I used a Factory over a service but you can read about why to use one over the other
A couple of things to keep in mind. You geolcation is more of a service then something to control since you're requesting from the user, you could add it to the below factory to expand the factories capabilities and allow you to use getLocation in other controllers.
make sure you add the distance service to your html document
also make sure you put the distance service in your controller and on the main angular app.
distanceService.js
(function(){
'use strict'
var distSrvc= angular.module("distanceService",[])
distSrvc.factory('DistanceCalc',['$http',function($http){
return{
getDistance:function(facilitiesData,locationData){
Object.keys(facilitiesData).length; // or if it's already an array of facilities use facilitiesData.length
// if its a javascript object and distance is already defined
for (var distance in facilitiesData) {
if (facilitiesData.hasOwnProperty(distance)) {
facilitiesData.distance = distCalc(locationData.lat,locationData.lng,facilitieData.location.lat,facilitieData.location.lng);
}
}
// if its an array of objects
for (var i = 0 ; i< facilitiesData.length;i++){
facilitiesData[i].distance = distCalc(locationData.lat,locationData.lng,facilitieData.location.lat,facilitieData.location.lng);
}
return facilitiesData
}
})();
Then in your controller you'll need to load the service for use.
yourcontroller.js this will give errors if you don't load it on the html page and add it to the main angular app.
(function(){
'use strict'
var myController= angular.module("distanceService",[])
myController.controller('myController',['$scope','DistanceCalc',function($http,DistanceCalc){ // see how i added the service and passed it to the function
$scope.getLocation = function(){
if (navigator.geolocation)
{
navigator.geolocation.getCurrentPosition(function (position){
$scope.location = {lat: position.coords.latitude, lng: position.coords.longitude};
$scope.$broadcast('location_obtained');
$scope.buildDist();
$scope.fetch();
//$scope.getRec();
});
}
else{
alert("Geolocation is not supported by this browser.");
}
};
// this line below sends the data to the service
DistanceCalc.getDistance($scope.facilities,$scope.location)
.success(function(data){
//success i'm done running distance calculations and now i want to update the facilties object with the added distance
$scope.facilities = data
})
.error(function(data){
// some error occurred and data can tell you about that error
})
}
})();

Meteor Users collection and Deps.autorun problems

I'm still struggling to understand how to access Meteor.users as a foreign key from another collection query. I understand that only the current user is published by default so I have a publication on the server as
Meteor.publish('itemOwner', function(userId) {
check(userId, String);
var user = Meteor.users.find({id: userId});
return user;
// return Meteor.users.find({id: userId}, {
// fields: {'profile': 1} });
});
I then have a Deps.autorun on the client..
Deps.autorun(function () {
var itemOwnerId = Session.get("itemOwnerID");
if (itemOwnerId) {
debugger
var ID = Session.get("itemOwnerID");
Meteor.subscribe('itemOwner', Session.get("itemOwnerID"));
}
});
I set the session ID on a modal form load, and display it in the template by calling the ownerProfile helper (or try to)
Template.showQuoteModalInner.helpers({
getQuote: function () {
// Get the quote ID from the session var
var quote = Session.get("quoteID");
if(quote) {
debugger;
var ID = quote.user._id;
Session.set("itemOwnerID", quote.user._id);
return quote;
}
},
ownerProfile: function() {
debugger;
var quote = Session.get("quoteID");
if(quote) {
var ID = quote.user._id;
var theUser = Meteor.users.find({_id: quote.user._id});
return theUser;
};
}
});
Now, I can trace the user ID at each stage and see it getting correctly passed to the autorun and the helpers. If I stop the program at the debugger in the ownerProfile helper and in the console put in Meteor.user.fetch({_id: "the id here"}).fetch() I get the correct user back.. but, in the handler itself the Meteor.users.find returns null??? What am I missing?
Two possibilities I noticed.
First, you are missing an underscore in the find in your publish function.
.find({id: userId}) should be .find({_id: userId}).
But this probably isn't the issue if you are seeing the user (other than the logged in user) in the console.
Second, if you are not seeing the user from your Template.showQuoteModalInner.ownerProfile helper, it is probably because you are returning a find() instead of a findOne().
find() returns a cursor whereas findOne() returns the record. Try findOne() if you want to display that single user's attributes.

Backbone.js - problem when saving a model before previous save issues POST(create) instead of PUT(update) request

I've developed a nice rich application interface using Backbone.js where users can add objects very quickly, and then start updating properties of those objects by simply tabbing to the relevant fields. The problem I am having is that sometimes the user beats the server to its initial save and we end up saving two objects.
An example of how to recreate this problem is as follows:
User clicks the Add person button, we add this to the DOM but don't save anything yet as we don't have any data yet.
person = new Person();
User enters a value into the Name field, and tabs out of it (name field loses focus). This triggers a save so that we update the model on the server. As the model is new, Backbone.js will automatically issue a POST (create) request to the server.
person.set ({ name: 'John' });
person.save(); // create new model
User then very quickly types into the age field they have tabbed into, enters 20 and tabs to the next field (age therefore loses focus). This again triggers a save so that we update the model on the server.
person.set ({ age: 20 });
person.save(); // update the model
So we would expect in this scenario one POST request to create the model, and one PUT requests to update the model.
However, if the first request is still being processed and we have not had a response before the code in point 3 above has run, then what we actually get is two POST requests and thus two objects created instead of one.
So my question is whether there is some best practice way of dealing with this problem and Backbone.js? Or, should Backbone.js have a queuing system for save actions so that one request is not sent until the previous request on that object has succeeded/failed? Or, alternatively should I build something to handle this gracefully by either sending only one create request instead of multiple update requests, perhaps use throttling of some sort, or check if the Backbone model is in the middle of a request and wait until that request is completed.
Your advice on how to deal with this issue would be appreciated.
And I'm happy to take a stab at implementing some sort of queuing system, although you may need to put up with my code which just won't be as well formed as the existing code base!
I have tested and devised a patch solution, inspired by both #Paul and #Julien who posted in this thread. Here is the code:
(function() {
function proxyAjaxEvent(event, options, dit) {
var eventCallback = options[event];
options[event] = function() {
// check if callback for event exists and if so pass on request
if (eventCallback) { eventCallback(arguments) }
dit.processQueue(); // move onto next save request in the queue
}
}
Backbone.Model.prototype._save = Backbone.Model.prototype.save;
Backbone.Model.prototype.save = function( attrs, options ) {
if (!options) { options = {}; }
if (this.saving) {
this.saveQueue = this.saveQueue || new Array();
this.saveQueue.push({ attrs: _.extend({}, this.attributes, attrs), options: options });
} else {
this.saving = true;
proxyAjaxEvent('success', options, this);
proxyAjaxEvent('error', options, this);
Backbone.Model.prototype._save.call( this, attrs, options );
}
}
Backbone.Model.prototype.processQueue = function() {
if (this.saveQueue && this.saveQueue.length) {
var saveArgs = this.saveQueue.shift();
proxyAjaxEvent('success', saveArgs.options, this);
proxyAjaxEvent('error', saveArgs.options, this);
Backbone.Model.prototype._save.call( this, saveArgs.attrs, saveArgs.options );
} else {
this.saving = false;
}
}
})();
The reason this works is as follows:
When an update or create request method on a model is still being executed, the next request is simply put in a queue to be processed when one of the callbacks for error or success are called.
The attributes at the time of the request are stored in an attribute array and passed to the next save request. This therefore means that when the server responds with an updated model for the first request, the updated attributes from the queued request are not lost.
I have uploaded a Gist which can be forked here.
A light-weight solution would be to monkey-patch Backbone.Model.save, so you'll only try to create the model once; any further saves should be deferred until the model has an id. Something like this should work?
Backbone.Model.prototype._save = Backbone.Model.prototype.save;
Backbone.Model.prototype.save = function( attrs, options ) {
if ( this.isNew() && this.request ) {
var dit = this, args = arguments;
$.when( this.request ).always( function() {
Backbone.Model.prototype._save.apply( dit, args );
} );
}
else {
this.request = Backbone.Model.prototype._save.apply( this, arguments );
}
};
I have some code I call EventedModel:
EventedModel = Backbone.Model.extend({
save: function(attrs, options) {
var complete, self, success, value;
self = this;
options || (options = {});
success = options.success;
options.success = function(resp) {
self.trigger("save:success", self);
if (success) {
return success(self, resp);
}
};
complete = options.complete;
options.complete = function(resp) {
self.trigger("save:complete", self);
if (complete) {
return complete(self, resp);
}
};
this.trigger("save", this);
value = Backbone.Model.prototype.save.call(this, attrs, options);
return value;
}
});
You can use it as a backbone model. But it will trigger save and save:complete. You can boost this a little:
EventedSynchroneModel = Backbone.Model.extend({
save: function(attrs, options) {
var complete, self, success, value;
if(this.saving){
if(this.needsUpdate){
this.needsUpdate = {
attrs: _.extend(this.needsUpdate, attrs),
options: _.extend(this.needsUpdate, options)};
}else {
this.needsUpdate = { attrs: attrs, options: options };
}
return;
}
self = this;
options || (options = {});
success = options.success;
options.success = function(resp) {
self.trigger("save:success", self);
if (success) {
return success(self, resp);
}
};
complete = options.complete;
options.complete = function(resp) {
self.trigger("save:complete", self);
//call previous callback if any
if (complete) {
complete(self, resp);
}
this.saving = false;
if(self.needsUpdate){
self.save(self.needsUpdate.attrs, self.needsUpdate.options);
self.needsUpdate = null;
}
};
this.trigger("save", this);
// we are saving
this.saving = true;
value = Backbone.Model.prototype.save.call(this, attrs, options);
return value;
}
});
(untested code)
Upon the first save call it will save the record normally. If you quickly do a new save it will buffer that call (merging the different attributes and options into a single call). Once the first save succeed, you go forward with the second save.
As an alternative to the above answer, you could achieve the same affect by overloading the backbone.sync method to be synchronous for this model. Doing so would force each call to wait for the previous to finish.
Another option would be to just do the sets when the user is filing things out and do one save at the end. That well also reduce the amount of requests the app makes as well

Categories

Resources