Reputation: 4373
I am using closures to ensure that just one of many events bound to an element are acted on, eg: $('.textInputs').on('keyup.my.evt change.my.evt', once(options));
I am using underscore's _.debounce
inside of a once
function closure. Everything works if I make a change in one input and wait before making a change in another input. If I make a change in one input and quickly tab to the next and make a change in a second before the time wait expires on the first input then the debounced function is only executed for the second input. This tells me that both inputs are using the same function returned from debounce but I am not sure how to change my code structure so that each input uses their own debounced function.
The debounced function:
var doStuff = function(eleObj, options){ console.log(eleObj, options); } // the function that does stuff
var debouncedDoStuff = _debounce(doStuff, 2000); // debouncing doStuff()
The once closure definition:
var once = function (options) {
var eventType = null;
function doOnce(e) {
if (!eventType) {
eventType = e.type; // only the first eventType we get will be registered
}
if (e.type == eventType) {
debouncedDoStuff($(this), options); // calling the debounced do stuff func
}
}
return doOnce;
}
Doc ready:
$(document).ready(function(){
var options = {foo:'bar', baz:'biz'};
$('.textInputs').on('keyup.my.evt change.my.evt', once(options));
});
I made a fiddle that demonstrates the problem: https://jsfiddle.net/enzt7q90/
Clarification Notes The code above has been reduced to the bare minimum showing the problem. The real-world goal is to have doStuff attached to an ajax process that saves any changes made to tabular data.
I should also point out that the once closure is not like jQuery's .one(). For instance, .one('keyup change', function(){}) will exec the func twice when the tab key is pressed (once for keyup and again for change) whereas the way I am using the once closure the function is only exec'd once for those two events.
Upvotes: 0
Views: 735
Reputation: 4366
You create a debounced version of doStuff
by passing it through _.debounce
. Your problem is that you do this only once, so there is only one such debounced version that the event handler must share between all elements. Debouncing means that only some calls actually get through to the original function. Essentially, you are hiding all calls except for the most recent one from doStuff
.
I'm getting the impression that by the time you actually trigger doStuff
, you want to take into account all input elements that have changes. You want to process each of those elements only once, regardless of how many times it triggered your event handler. This means that the processing (doStuff
) should be debounced, as you did, but that the immediate event handler (doOnce
) must somehow distinguish between input elements.
One possible solution was already commented by Boudy hesham, i.e., to register a separate event handler with a separate debounced version of doStuff
for each input:
var doStuff = function(eleObj, options){ console.log(eleObj, options); } // same as before
function once(options) {
// Note that the debounced function is now generated inside the
// event handler factory.
var debouncedDoStuff = _.debounce(doStuff, 2000);
function doOnce(e) {
// We don't need to restrict the event type anymore, because
// this event handler is specific to a single input element.
// The debounce already ensures that `doStuff` is called only once
// for this element.
debouncedDoStuff($(this), options);
}
return doOnce;
}
$(document).ready(function(){
var options = {foo:'bar', baz:'biz'};
// Using jQuery's .each to generate an event handler for each input
// element individually.
$('.textInputs').each(function() {
$(this).on('keyup.my.evt change.my.evt', once(options));
});
});
Another option is to keep a list of all the input elements that have triggered the event, and deduplicate them when you finally run doStuff
after the timeout:
var doStuff = function(elementList, options){
var uniqueElements = _.uniq(elementList);
console.log(uniqueElements, options);
}
var debouncedDoStuff = _debounce(doStuff, 2000); // as original.
function once(options) {
var targetList = [];
function doOnce() {
targetList.push($(this));
debouncedDoStuff(targetList, options);
// Note that again, I don't restrict the callback to a single
// event type, because the debouncing and the deduplication
// already ensure that each input is processed only once.
// This also prevents a problem that might ignore a 'keyup' event
// in one element if 'change' was already triggered in another.
}
return doOnce;
}
// The next part is the same as your original code.
$(document).ready(function(){
var options = {foo:'bar', baz:'biz'};
$('.textInputs').on('keyup.my.evt change.my.evt', once(options));
});
The latter option is perhaps a bit leaner because you have fewer closures, but this probably won't really matter unless you have hundreds of input elements.
A third option would be to use a framework to structure your event handling in a conventional way. Backbone is an example of a framework that goes well with jQuery and Underscore and it can handle this kind of situation well. Getting handy with a framework is beyond the scope of this answer, though.
Upvotes: 2