I'm looking for an elegant, efficient solution to my problem :
I have this webapp with many components;
One main component include many additions that will grow/evolve with time.
This main component has a function in which before actually doing what it is supposed to do, it is triggering an event beforedo so that the addons can listen to.
dostg : function () {
$doc.trigger('beforedo');
//do stuff but after event is handled by the addons ?
}
In the addons codes
$doc.on('beforedo',function(e) {
//do before addon stuff
}
Now those before doing stuff may involve ajax request or anything that would take some processing time.
I could increment a counter on ajax requests and wait it is down to nil, but what I'd like is a solution that would just wait for all handlers to finish their job (and therefore that'd work whatever is being done in those addons handlers).
Is there some miracle solution to do this or I have to forget about event in this situation and go another way (array of function to iterate, addons pushing new function into array) ?
Thanks for your expertise!
-------EDIT FOLLOWING THE BOUNTY
Apologize to @xbonez & @Sergiu Paraschiv , I should have edit the question with the solution I'm using now before offering the bounty (solution I'm not totally satisfied with, hence the bounty).
//app
var $doc = $(document.body);
//component
$doc.on({
wait: function (e) { ++$docponent.handleinprogress; },
addonready: function () {
--$docponent.handleinprogress;
if ($docponent.handleinprogress==0) $doc.trigger('readyfordo');
},
readyfordo: function () {
//do stuff after event has been handled by the addons
}
});
$docponent = {
handleinprogress: 0,
dostg: function () {
$docponent.handleinprogress = 0;
$doc.trigger('beforedo');
}
};
//in component addon files
$doc.on('beforedo', function (e) {
$doc.trigger('wait');
//do addon stuff
$doc.trigger("addonready");
}
I'm not satisfied with this solution because even if I don't need to do stuff in the addon beforedo I still have to add the handler to trigger addonready (in at least one of the addon -> so either i lose flexibility to add/remove addon from the component without worrying whether readyfordo get triggered, either I have to include the handler in every addon - mostly -75%- for nothing).
I'm looking for an elegant, efficient solution to my problem :
I have this webapp with many components;
One main component include many additions that will grow/evolve with time.
This main component has a function in which before actually doing what it is supposed to do, it is triggering an event beforedo so that the addons can listen to.
dostg : function () {
$doc.trigger('beforedo');
//do stuff but after event is handled by the addons ?
}
In the addons codes
$doc.on('beforedo',function(e) {
//do before addon stuff
}
Now those before doing stuff may involve ajax request or anything that would take some processing time.
I could increment a counter on ajax requests and wait it is down to nil, but what I'd like is a solution that would just wait for all handlers to finish their job (and therefore that'd work whatever is being done in those addons handlers).
Is there some miracle solution to do this or I have to forget about event in this situation and go another way (array of function to iterate, addons pushing new function into array) ?
Thanks for your expertise!
-------EDIT FOLLOWING THE BOUNTY
Apologize to @xbonez & @Sergiu Paraschiv , I should have edit the question with the solution I'm using now before offering the bounty (solution I'm not totally satisfied with, hence the bounty).
//app
var $doc = $(document.body);
//component
$doc.on({
wait: function (e) { ++$doc.component.handleinprogress; },
addonready: function () {
--$doc.component.handleinprogress;
if ($doc.component.handleinprogress==0) $doc.trigger('readyfordo');
},
readyfordo: function () {
//do stuff after event has been handled by the addons
}
});
$doc.component = {
handleinprogress: 0,
dostg: function () {
$doc.component.handleinprogress = 0;
$doc.trigger('beforedo');
}
};
//in component addon files
$doc.on('beforedo', function (e) {
$doc.trigger('wait');
//do addon stuff
$doc.trigger("addonready");
}
I'm not satisfied with this solution because even if I don't need to do stuff in the addon beforedo I still have to add the handler to trigger addonready (in at least one of the addon -> so either i lose flexibility to add/remove addon from the component without worrying whether readyfordo get triggered, either I have to include the handler in every addon - mostly -75%- for nothing).
Share Improve this question edited Sep 19, 2013 at 14:14 123 5251 gold badge5 silver badges20 bronze badges asked May 16, 2013 at 7:59 mikakunmikakun 2,2652 gold badges18 silver badges24 bronze badges4 Answers
Reset to default 8To wait for all handlers to finish before executing some code, you should use jQuery's deferred
API. You can do something like this:
$.when($.ajax("/page1.php"), $.ajax("/page2.php")).done(function(a1, a2){
// only executes after both ajax calls have completed
});
Further, jQuery's trigger
allows you to pass extra params. Pass in a function which will be the callback function.
Your final code should look something like this:
$doc.trigger('beforedo', function() {
// anything here only executes after the event has been handled
});
$doc.on('beforedo',function(e, callback) {
//do before addon stuff
$.when($.ajax("/page1.php"), $.ajax("/page2.php")).done(function(a1, a2){
// at this point, all your AJAX calls have completed
// call the callback function
callback();
});
}
If need be, when you call callback()
you can even pass in any result you might need to pass as an argument. Accordingly, change the function signature in your .trigger()
call too.
Expanding on the comment I left below, you can have a common loader function if you like:
$.when(load("page1"), load("page2")).done(function(){
// ...
});
function load(file) {
// return the $.ajax promise
return $.ajax(file);
}
See:
jQuery deferred
jQuery.when()
jQuery.trigger
Check out my fiddle here. Your idea of counting finished requests is good, it's just a matter of structuring code. Knowing that you need decoupled "modules/addons" this is the way I'd go:
var $doc = $(document);
function Main() {
var handlersCount = 0;
var completedHandlers = 0;
function afterDostg() {
console.log('after addon stuff');
}
this.dostg = function() {
$doc.trigger('beforeDo');
};
this.addHandler = function() {
handlersCount++;
};
this.handleCompleted = function() {
completedHandlers++;
if(completedHandlers === handlersCount) {
completedHandlers = 0;
afterDostg();
}
}
}
function Addon1(main) {
main.addHandler();
$doc.on('beforeDo', function(e) {
console.log('addon1 stuff');
main.handleCompleted();
});
}
function Addon2(main) {
main.addHandler();
$doc.on('beforeDo', function(e) {
setTimeout(
function() {
console.log('addon2 stuff');
main.handleCompleted();
},
1000
);
});
}
var main = new Main();
var addon1 = new Addon1(main);
var addon2 = new Addon2(main);
main.dostg();
Using this method, addons can have anything in them, they just have to notify "Main" when they finish whatever they need to do.
If I were you I'd go even further and extract the whole "handlers" code in "Main" in a separate class instantiated as a public property in "Main" with afterDostg
as a parameter. That way you don't polute app code with meta stuff like this.
Softlion pointed out several points, with which I agree.
Potential problems :
One problem may arise with your current implementation, if one of your addons calls $doc.trigger("addonready");
synchronously :
// addon 1 :
$doc.on('beforedo',function(e){
$doc.trigger('wait');
//do synchronous stuff :
$('body').append('<div class="progressbar"></div>');
console.log('addon 1 initialized');
$doc.trigger("addonready");
}
// addon 2 :
$doc.on('beforedo',function(e){
$doc.trigger('wait');
$.ajax({ ...
complete: function(){
console.log('addon 2 initialized');
$doc.trigger("addonready");
}
});
}
In this case, depending on the resolution order of your callbacks, you may accidentally trigger your readyfordo
event after the first addon has triggered its addonready
function, and before the second one has had a change to trigger wait
.
Your code also relies on the asumption that all of your addons will always execute exactly one .trigger('addonready')
for each .trigger('wait')
. I don't know how your code looks like, or how many addons you have, but ensuring that this is the case for each possible execution path is quite a hurdle (e.g. : have you tested the 2^n
failure cases if you have n
ajax calls ?)
As long as all your code is in-house, you may have control over it, but it does seem brittle to me.
jQuery's Deferreds / Promises :
A generic pattern is to use jQuery's promises. All of jQuery's asynchronous calls are now wrapped in promises, and the library offers an API which will allow to manipulate those in a rather elegant way - plus it probably has been tested in more corner cases than your own code.
Here are my 2 cents :
$doc.component = {
initQueue: [], //this array will contain callbacks, each of which
//is expected to return a promise
dostg : function () {
var queue = $doc.component.initQueue;
var promises = [];
var i, fnInit;
for(i=0; i<queue.length; i++){
fnInit = queue[i];
//safeguard, maybe useless :
if (typeof(fnInit) !== 'function') { continue; }
var obj = fnInit();
// we stack together all return values in an array :
promises.push( obj );
}
// wait for all that should be waited for, then trigger your "main" event :
$.when.apply($, promises).then(function(){ $doc.trigger('readyfordo'); });
// $.when sorts out between promises and other results, and waits
// only if some promises are not resolved.
}
};
//in component addon files
$doc.component.initQueue.push(function(){
//do addon stuff
//if waiting for asynch event, return a promise.
//examples :
// jQuery ajax functions already return a Deferred :
return $.ajax({ ... }); // just don't forget the 'return' ...
return $.get(url, {...}, function(){ ... });
//or, if you need a custom promise, Softlion's example :
var deferred = $.Deferred();
doasyncthing(function() { deferred.resolve(); });
return deferred.promise();
});
Instead of using your beforedo
,wait
,addonready
events, you have your plugins register a function in an initQueue
known by your component - note that you can choose not to register the callback if your addon doesn't need one.
Roughly speaking : in your addons, you replace $doc.on('beforeDo', function(){ ... })
with $doc.component.initQueue.push(function(){ ... })
, and if you need to wait for something, you return a promise around this something.
You can then let the $.when()
function take care of bundling all together and waiting for what should be waited for.
UPDATE : actually, $.when
expects the promises as separate arguments
If stored in an array, you need to call $.when.apply($, array)
to fit the function's signature.
$.when(array)
will consider that its argument (the array) is not a Promise, and resolve immediately.
fiddle
The solution your are using is not working properly. If you have 2 addons, the 1st receive the event and register 'wait', then the same one calls readyfordo. What happens with the 2nd ? It won't have the chance to init.
Each of your addons must add itself in a global array, and when you throw your event you should handle the case when there is nothing in the array, otherwise you use the same code in your solution without using the handleinprogress counter, just use the length of the array instead.
You may also be interested in the promise pattern (see Deferred object in jquery doc) which make waiting asynchronously for an "event" a breeze.
http://api.jquery.com/category/deferred-object/
http://api.jquery.com/deferred.promise/
var plugins = [];
$.someplugin = function() {
plugins.push(this);
this.init = function() {
//do some things synchronously
//start async things. The callback is called when the async thing is finished.
var deferred = $.Deferred();
doasyncthing(function() { deferred.resolve(); });
//return a promise
return deferred.promise();
}
}
Caller:
function throwEventAndWaitAsync(callback) {
var promises = [];
for(plugin in plugins) {
promises.push(plugin.init());
}
//Wait on all promises asynchronously then call callback()
$.when(promises).then(callback);
}
as simple as that.