How to queue checkbox clicks before making http request? - javascript

I have a series of checkboxes and want to make an $http request when the user checks a checkbox. However, since the http request takes some time to complete and the user may want to check a few different boxes at a time, I want to delay sending the http request until the user has stopped checking boxes.
I have tried using $timeout to set a delay before sending the $http request, but this doesn't really queue the selections before sending.
var nodeList = [];
$scope.checkNode = function(checkedNode, sendRequestFn) {
nodeList.push(checkedNode);
$timeout(function() {
sendRequestFn(nodeList);
nodeList = []; // Sent the list, so reset this.
}, 500);
}

Clear your timeout each time a button is clicked. With your implementation you are starting a new timeout each click.
var nodeList = [], waitBeforeRequest;
$scope.checkNode = function(checkedNode, sendRequestFn) {
// if we are waiting, reset the timer
if (waitBeforeRequest) $timeout.cancel(waitBeforeRequest);
// wait for any more clicks before sending
waitBeforeRequest = $timeout(function () {
sendRequestFn(nodeList);
nodeList = [];
}, 500)
}

You need to know when the user is done with the form and is ready to submit their checkbox changes. So, you should tie the http request to a submit button or button click of some kind, and the user will click on this button when done, triggering the update.
If you don't want to tie the http update to a button click, then I think tying the event to a timeout is your second best option, since there's no way to tell when a user is done with input.

I think what you're after is called a debounce function. You can write your own, but it is available in the underscore library, that is usually fine to include in the page along with Angular. Example usage would be:
var nodeList = [];
var debouncedSend = _.debounce(function(sendRequestFn) {
sendRequestFn(nodeList);
nodeList = [];
}, 500);
$scope.checkNode = function(checkedNode, sendRequestFn) {
nodeList.push(checkedNode);
debouncedSend(sendRequestFn);
};
The debouncedSend would be called outside of Angular, so you might need to wrap the function in $apply, as in:
var debouncedSend = _.debounce(function(sendRequestFn) {
$scope.$apply(function() {
sendRequestFn(nodeList);
nodeList = [];
});
}, 500);

Related

JS Foreach Alternative (timeout) for Queue

I'm working on the "Approve All" button. The process here is when I click "Approve All," each individual "Approve" button will be triggered as "click" all at once, and then it will send POST requests to the controller. However, when I clicked Approve All button, there was a race condition causing the controller returns Error 500: Internal server error. I have tried using JS setTimeout() with value 1500*iter, but when the iterator gets higher, for example at i = 100, then it would take 1500*100 => 150000ms (150s). I hope that explains the problem clearly. Is there a way to prevent such a case?
Here is my code, I'm using JQuery:
let inspection = $this.parents("li").find("ul button.approve"); // this will get all 'approve' button to be clicked at once
inspection.each((i,e)=>{
(function () {
setTimeout(function () {
$(e).data("note",r);
$(e).click();
}, 1500 * i); // this acts like a queue, but when i > 100, it takes even longer to send POST requests.
})(this,i,e,r);
});
// then, each iteration will send a POST request to the controller.
$("#data-inspection ul button.approve").on("click", function() {
// send POST requests
});
Any help would be much appreciated. Thank you.
That 500 error may also be the server crashing from being unable to process all the requests simultaneously.
What I'd recommend is using an event-driven approach instead of setTimeout. Your 1500ms is basically a guess - you don't know whether clicks will happen too quickly, or if you'll leave users waiting unnecessarily.
I'll demonstrate without jQuery how to do it, and leave the jQuery implementation up to you:
// use a .js- class to target buttons your buttons directly,
// simplifying your selectors, and making them DOM agnostic
const buttonEls = document.querySelectorAll('.js-my-button');
const buttonsContainer = document.querySelector('.js-buttons-container');
const startRequestsEvent = new CustomEvent('customrequestsuccess');
// convert the DOMCollection to an array when passing it in
const handleRequestSuccess = dispatchNextClickFactory([...buttonEls]);
buttonsContainer.addEventListener('click', handleButtonClick);
buttonsContainer.addEventListener(
'customrequestsuccess',
handleRequestSuccess
);
// start the requests by dispatching the event buttonsContainer
// is listening for
buttonsContainer.dispatchEvent(startRequestsEvent);
// This function is a closure:
// - it accepts an argument
// - it returns a new function (the actual event listener)
// - the returned function has access to the variables defined
// in its outer scope
// Note that we don't care what elements are passed in - all we
// know is that we have a list of elements
function dispatchNextClickFactory(elements) {
let pendingElements = [...elements];
function dispatchNextClick() {
// get the first element that hasn't been clicked
const element = pendingElements.find(Boolean);
if (element) {
const clickEvent = new MouseEvent('click', {bubbles: true});
// dispatch a click on the element
element.dispatchEvent(clickEvent);
// remove the element from the pending elements
pendingElements = pendingElements.filter((_, i) => i > 0);
}
}
return dispatchNextClick;
}
// use event delegation to mitigate adding n number of listeners to
// n number of buttons - attach to a common parent
function handleButtonClick(event => {
const {target} = event
if (target.classList.contains('js-my-button')) {
fetch(myUrl)
.then(() => {
// dispatch event to DOM indicating request is complete when the
// request succeeds
const completeEvent = new CustomEvent('customrequestsuccess');
target.dispatchEvent(completeEvent);
})
}
})
There are a number of improvements that can be made here, but the main ideas here are that:
one should avoid magic numbers - we don't know how slowly or quickly requests are going to be processed
requests are asynchronous - we can determine explicitly when they succeed or fail
DOM events are powerful
when a DOM event is handled, we do something with the event
when some event happens that we want other things to know about, we can dispatch custom events. We can attach as many handlers to as many elements as we want for each event we dispatch - it's just an event, and any element may do anything with that event. e.g. we could make every element in the DOM flash if we wanted to by attaching a listener to every element for a specific event
Note: this code is untested

handle multiple angular calls when more submit buttons available

I have a form which contains one drop down, check box and four buttons. When any of the action is performed (check/uncheck or drop down selection or button click), it has to trigger a service call and the below section should be updated. There may be a chance that the multiple actions can be performed one after other immediately. If this is the case, http call should be triggered only after last activity.
Would be nice if there is any workable idea for this. I feel the timeout can helpful to wait for user to complete all activities (waiting for certain time after each activity) and call http service.
I guess thats what you need:
var timeoutPromise;
var delayInMs = 2000;
$scope.$watch("your_form_scope", function(newValue, oldValue) {
$timeout.cancel(timeoutPromise); //does nothing, if timeout alrdy done
timeoutPromise = $timeout(function(){ //Set timeout
//your code
},delayInMs);
});

Executing current ajax call and aborting all previous ones

I am trying to build an auto-complete UI. There is an input whose on keyup function does an ajax call to server to fetch the most relevant data. But if user types a word which is, say 10 character long, so for each keyup one ajax call is made and my dialogue box refreshes 10 times.
I have tried using abort() for the ajax call. When I do an abort to previous ajax call, the call is not made but still it waits for 10 calls before executing the last one, which makes the user experience very bad.
So is there a way to execute just the current ajax call without any delay from the previous ones?
A part of my code:
var request_autocomplete=jQuery.ajax({});
$('.review_autocomplete').keyup(function() {
request_autocomplete.abort();
request_autocomplete=jQuery.ajax({
// DO something
});
});
OP, there are two parts to this. The first is your abort, which it seems that you already have.
The second is to introduce forgiveness into the process. You want to fire when the user stops typing, and not on every key press.
You need to use both keyUp and keyDown. On keyUp, set a timeout to fire your submit. Give it perhaps 700ms. On KeyDown, clear the timeout.
var request_autocomplete=jQuery.ajax({});
var forgiveness;
// first your AJAX routine as a function
var myServiceCall = function() {
request_autocomplete.abort();
request_autocomplete=jQuery.ajax({
// DO something
}
// keyup
$('.review_autocomplete').keyup(function() {
forgiveness = window.setTimeout(myServiceCall, 700);
});
});
// key down
$('.review_autocomplete').keydown(function() {
window.clearTimeout(forgiveness);
});
});
What this will do is constantly set a timeout to fire every time a key is up, but each time a key is down it will cancel that timeout. This will have the effect of keeping your service call from firing until the user has stopped typing, or paused too long. The end result is that you will wind up aborting a much smaller percentage of your calls.
you can implement the way you asked in your question is preventing for example 3 calls as below :
var calls = 0;
$('.review_autocomplete').keyup(function() {
if (calls >3) {
request_autocomplete.abort();
request_autocomplete=jQuery.ajax({
// DO something
});
calls = 0;
}
calls++;
});
but this way not recommended because when user wants to types sample while user types samp at p ajax call fire up. and when user type l and e nothing happen !
If you are using jquery Autocomplete
you can using
minLenght so you can check current lenght of text box and when user typed at least 3 character then you must call the ajax request.
delay (between last keystroke and ajax call. Usually 2-300ms should do)
and using AjaxQueue
after a quick search about this issue I have found this link that shows another way to prevent multiple ajax calls for autocomplete by using cache
You could use a globalTimeout variable that you reset with setTimeout() and clearTimeout().
var globalTimeout;
$('.review_autocomplete').keydown(function(){
if(globalTimeout)clearTimeout(globalTimeout);
}).keyup(function(){
globalTimeout = setTimeoout(function(){
$.ajax({/* you know the drill */});
}, 10);
});
This way the timeout is cleared whenever your Client pushes a keydown, yet the timeout is set again as soon as the your Client releases a key onkeyup, therefore $.ajax() will only be called if there's no key action, after 10 milliseconds in this case. I admit that this won't stop an $.ajax() call that has already been made, however it probably won't matter because they happen pretty fast, and because this example prevents future $.ajax() calls as long as the Client keeps typing.
Try
var count = {
"start": 0,
// future , by margin of `count.timeout`
"complete": 0,
// if no `keyup` events occur ,
// within span of `count.timeout`
// `request_autocomplete()` is called
// approximately `2` seconds , below ,
// adjustable
"timeout" : 2
};
$('.review_autocomplete')
.focus()
.on("keyup", function (e) {
elem = $(this);
window.clearInterval(window.s);
window.s = null;
var time = function () {
var t = Math.round($.now() / 1000);
count.start = t;
count.complete = t + count.timeout;
};
time();
var request_autocomplete = function () {
return jQuery.ajax({
url: "/echo/json/",
type: "POST",
dataType: "json",
data: {
json: JSON.stringify({
"data": elem.val()
})
}
// DO something
}).done(function (data) {
window.clearInterval(s);
console.log("request complete", data);
$("body").append("<br /><em>" + data.data + "</em>");
elem.val("");
count.start = count.complete = 0;
console.log(count.start, count.complete);
});
};
window.s = setInterval(function () {
if (Math.round($.now() / 1000) > count.complete) {
request_autocomplete();
console.log("requesting data");
};
// increased to `1000` from `501`
}, 1000);
});
jsfiddle http://jsfiddle.net/guest271314/73yrndwy/

Is it a bad idea to trigger a database lookup on keyup (i.e. checking for unique username)

I have a field where a user can manually enter a code for a new product. It cannot be a db-generated ID as the user needs control over the code.
As this will require a round-trip to the server to check, my intention was to trigger this check as the "keyup" event happens (a bit like a live filter would work) so that there is instantaneous feedback about the entered code (and probably CSS-based colour/image reinforcement). Admittedly for this particular function I may choose to do it onblur instead of keyup but there may well be other situations where I require a check on keyup.
Is this fundamentally a bad thing or not? A round-trip to the server to check if an item code exists (primary key) should be a very fast process but with a slow connection I'm just wondering if there could be a race condition whereby a fast typer stacks up the db calls faster than it can return and then a situation may emerge whereby a "submit" button is enabled when it shouldn't be. Of course I would also back this up with db-level checking on submission of the form, but I'm trying to make this as apparently responsive as possible.
Is it generally a bad idea to do keyup checking on any data source that's not held in memory or is this an acceptable practise?
Since you use knockout you can use throttle
ViewModel = function() {
this.text = ko.observable().extend({ throttle: 500 });
this.text.subscribe(this.onText, this);
};
ViewModel.prototype = {
onText: function(value) {
console.log("ajax call");
}
};
http://jsfiddle.net/TT3AB/
There's never going to be a race condition if you abort any previous ajax request on the next keyup.
EDIT
(function() {
var xhr; //a reference to an XMLHttpRequest Object
var onKeyUpCallback = function() {
if(xhr) {
xhr.abort();
}
xhr = ... //build your XHR object
//....
xhr.send();
}
yourInputElement.addEventListener('keyup',onkeyUpCallback);
}());
If you want to check at real time, you can approach in this way. When user types data and halts for a while then send you ajax request to check for existence of that particular thing in database.
Call a function on key up event but do not send request until user stops for a while
var timer;
function onkeyups()
{
if(timer)
{
clearTimeout(timer);
}
timer = setTimeout(function(){callRequest();},500) // the delay, after how much millisecond the ajax call should be made when user has stopped typing
}
function callRequest()
{
// Make your ajax call or whatever
}

Javascript - Which event to use for multiselect change

I'm using YUI as javascript framework, and can successfully react when the user changes the value of basic input fields, the reaction being to sent an Ajax query.
However, I'm not so lucky with multiselect dropdown lists:
listening to "change" would send my query each time the user adds/removes an item to his selection
listening to "blur" requires the user to click elsewhere in order to loose the focus and send the query (not very usable), plus it would send the query if the user only scrolls on the list without changing anything (useless, confusing).
Any idea (with YUI), that would use a clever behavior?
Or should I really listen to change and implement a timeout (to wait for subsequent changes before sending a query)?
I use the same kind of timeout you want on key events, to detect when the user have finished typing, the same approach can be used on your problem:
// helper function
var timeout = (function(){
var timer = 0;
return function(callback, ms){
clearTimeout (timer);
timer = setTimeout(callback, ms);
};
})();
Usage:
// YUI 2
YAHOO.util.Event.addListener(oElement, "change", function () {
timeout(function () {
// one second since the last selection change
}, 1000);
});
// YUI 3
Y.on("click", function () {
timeout(function () {
// one second since the last selection change
}, 1000);
}, oElement);
Basically in this timeout function, I reset the timer if the function is called before the specified delay.
you could run a setTimeout after the onChange event and keep track of a number of changes to determine whether or not a change had been made since the event was fired. if no changes were made within that time, then the query could be sent.
e.g., something like:
var changes = 0;
function myOnChangeHandler(e)
{
changes++;
var local_change = changes;
setTimeout(function() {
if (local_change === changes) {
sendRequest();
}
}, 500);
}

Categories