I'm writing a jQuery plugin and using .on and .trigger as my pub/sub system. However, I want to trigger multiple events in different scenarios.
Is this possible to do as one string, like the .on method?
Goal:
$this.trigger("success next etc"); // doesn't work
Current solution:
$this
.trigger("success")
.trigger("next")
.trigger("etc"); // works, triggers all three events
Any suggestions?
JQuery itself does not support triggering multiple events, however you could write custom extension method triggerAll
(function($) {
$.fn.extend({
triggerAll: function (events, params) {
var el = this, i, evts = events.split(' ');
for (i = 0; i < evts.length; i += 1) {
el.trigger(evts[i], params);
}
return el;
}
});
})(jQuery);
And call it like following:
$this.triggerAll("success next etc");
What you have is fine... you can't trigger multiple events using a comma separated list. The trigger() constructor only takes an event name and optional additional parameters to pass along to the event handler.
An alterternative would be to trigger all events attached to an element, however, this may not meet your needs if you need to trigger specific events in different senarios:
$.each($this.data('events'), function(k, v) {
$this.trigger(k);
});
Just in case anyone else stumbles upon this question later in life, I solved this by creating a custom jQuery function.
$.fn.triggerMultiple = function(list){
return this.each(function(){
var $this = $(this); // cache target
$.each(list.split(" "), function(k, v){ // split string and loop through params
$this.trigger(v); // trigger each passed param
});
});
};
$this.triggerMultiple("success next etc"); // triggers each event
You could extend the original .trigger() Method prototype:
(function($) {
const _trigger = $.fn.trigger;
$.fn.trigger = function(evtNames, data) {
evtNames = evtNames.trim();
if (/ /.test(evtNames)) {
evtNames.split(/ +/).forEach(n => _trigger.call(this, n, data));
return this;
}
return _trigger.apply(this, arguments);
};
}(jQuery));
$("body").on({
foo(e, data) { console.log(e, data); },
bar(e, data) { console.log(e, data); },
baz(e, data) { console.log(e, data); },
});
$("body").off("bar"); // Test: stop listening to "bar" EventName
$("body").trigger(" foo bar baz ", [{data: "lorem"}]); // foo, baz
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
Code explained
// Keep a reference to the original prototype
const _trigger = $.fn.trigger;
$.fn.trigger = function(evtNames, data) {
// Remove leading and ending whitespaces
evtNames = evtNames.trim();
// If the string has at least one whitespace
if (/ /.test(evtNames)) {
// Split names into Array (Treats multiple spaces as one)
evtNames.split(/ +/)
// Call the original .trigger() method for one eventName (and pass data)
.forEach(n => _trigger.call(this, n, data));
// End here.
// Return this (Element) to maintain jQuery methods chainability for this override.
return this;
}
// No whitespaces detected
// Pass all arguments to the original .trigger() Method immediately.
// The original method already returns this (Element), so we also need to
// return it here to maintain methods chainability when using this override.
return _trigger.apply(this, arguments);
};
Related
The problem is simple. I have a massive javascript application. And there are lot of times in the app where I use code which looks something like this -
$('#treat').html(new_data);
....
....
$('#cool').html(some_html_data);
....
....
$('#not_cool').html(ajax_data);
So what I want to do is, everytime this html() function is called I want to execute a set of functions.
function do_these_things_every_time_data_is_loaded_into_a_div()
{
$('select').customSelect();
$('input').changeStyle();
etc.
}
How do I do this? Thank you.
You can use the custom event handlers for that:
$('#treat').html(new_data);
// Trigger the custom event after html change
$('#treat').trigger('custom');
// Custom event handler
$('#treat').on('custom', function( event) {
// do_these_things_every_time_data_is_loaded_into_a_div
alert('Html had changed!');
});
UPDATE
Based on answer over here and with some modifications you can do this:
// create a reference to the old `.html()` function
$.fn.htmlOriginal = $.fn.html;
// redefine the `.html()` function to accept a callback
$.fn.html = function (html, callback) {
// run the old `.html()` function with the first parameter
this.htmlOriginal(html);
// run the callback (if it is defined)
if (typeof callback == "function") {
callback();
}
}
$("#treat").html(new_data, function () {
do_these_things_every_time_data_is_loaded_into_a_div();
});
$("#cool").html(new_data, function () {
do_these_things_every_time_data_is_loaded_into_a_div();
});
Easily maintainable and less code as per your requirements.
You can overwrite the jQuery.fn.html() method, as described in Override jQuery functions
For example, use this:
var oHtml = jQuery.fn.html;
jQuery.fn.html = function(value) {
if(typeof value !== "undefined")
{
jQuery('select').customSelect();
jQuery('input').changeStyle();
}
// Now go back to jQuery's original html()
return oHtml.apply(this, value);
};
When html() is called it usually make the DOM object changes, so you can look for DOM change event handler, it is called whenever your HTML of main page change. I found
Is there a JavaScript/jQuery DOM change listener?
if this help your cause.
You can replace the html function with your own function and then call the function html:
$.fn.html = (function(oldHtml) {
var _oldHtml = oldHtml;
return function(param) {
// your code
alert(param);
return _oldHtml.apply(this, [param]);
};
})($.fn.html);
I have a little script for you. Insert that into your javascript:
//#Author Karl-André Gagnon
$.hook = function(){
$.each(arguments, function(){
var fn = this
if(!$.fn['hooked'+fn]){
$.fn['hooked'+fn] = $.fn[fn];
$.fn[fn] = function(){
var r = $.fn['hooked'+fn].apply(this, arguments);
$(this).trigger(fn, arguments);
return r
}
}
})
}
This allow you to "hook" jQuery function and trigger an event when you call it.
Here how you use it, you first bind the function you want to trigger. In your case, it will be .html():
$.hook('html');
Then you add an event listener with .on. It there is no dynamicly added element, you can use direct binding, else, delegated evets work :
$(document).on('html', '#threat, #cool, #not_cool',function(){
alert('B');
})
The function will launch everytime #threat, #cool or #not_cool are calling .html.
The $.hook plugin is not fully texted, some bug may be here but for your HTML, it work.
Example : http://jsfiddle.net/5svVQ/
Here jsFiddle to test sample
I'm currently writing a jQuery snippet to handle any html content change in DOM 'triggered' by any jQuery domManip function (extending some functions). Not sure it's the best way to do it, so any advice will be welcome.
This snippet works as expected if bound to document. However, if I try to bind it to a specific element, I'm facing problem which some function as .remove(). Maybe it's due to custom event not using normal propagation behaviour but I'm really not sure.
This is a working sample, I bind contentChange event to document, works cross-browser as I can test it: {Firefox, IE9, Chrome and Safari under Win7}
;
(function ($) {
$.fn.contentChange = function (types, data, fn) {
return this.on('contentChange', types, null, data, fn);
};
var oDomManip = $.fn.domManip,
oHtml = $.fn.html,
oEmpty = $.fn.empty,
oRemove = $.fn.remove,
extendFct = function (oFct, sender, args) {
return oFct.apply(sender, args), $.event.trigger('contentChange');
//=>if testing specific element (#test) use instead following line
//return oFct.apply(sender, args), $(sender).trigger('contentChange');
};
$.fn.domManip = function () {
extendFct(oDomManip, this, arguments)
};
$.fn.html = function () {
extendFct(oHtml, this, arguments)
};
$.fn.empty = function () {
extendFct(oEmpty, this, arguments)
};
$.fn.remove = function () {
extendFct(oRemove, this, arguments)
};
})(jQuery);
I use: $.event.trigger('contentChange') to trigger custom event.
Called like it:
$(document).contentChange(function () {
console.log("onContentChange")
});
However, if I use:
$('#test').contentChange(function () {
console.log("onContentChange")
});
The custom event is not triggered.
So, to trigger a custom event on a specific element, I can triggered it like this:
$(sender).trigger('contentChange');
But now, call to remove() method on self or children doesn't triggered my custom event.
I can understand that event callback function won't be called if I remove the element, but why isn't it called when removing children (while it's working if bound to document!)?
I was expecting this line to make custom event bubbles to '#test':
$('#test').find('div:first').remove();
Is there any way to triggered this custom event bound to a specific element when manipulating this element and/or its children?
You need to trigger the event on the element that was modified.
http://jsfiddle.net/Gw4Lj/2/
return oFct.apply(sender, args), sender.trigger('contentChange');
however, with that change, you will no longer catch the event that was triggered on an element that isn't connected to the DOM because it isn't a descendant of that document, which is ok in my opinion because it isn't associated to that DOM, it's in a DOM Fragment.
I come with slightly modified version wich seems to work fine for the purpose i reach.
Need optimization for the .on() method extend, so please feel free to share your feedbacks.
Inspired from here: https://groups.google.com/forum/?fromgroups=#!topic/jquery-dev/ZaMw2XB6wyM
Thanks to Wil Stuckey
Here jsFiddle
;(function ($) {
var fctsToObserve = {
append: [$.fn.append, 'self'],
prepend: [$.fn.prepend, 'self'],
remove: [$.fn.remove, 'parent'],
before: [$.fn.before, 'parent'],
after: [$.fn.after, 'parent']
}, fctsObserveKeys = '';
$.each(fctsToObserve, function (key, element) {
fctsObserveKeys += "hasChanged." + key + " ";
});
var oOn = $.fn.on;
$.fn.on = function () {
if (arguments[0].indexOf('hasChanged') != -1) arguments[0] += " " + fctsObserveKeys;
return oOn.apply(this, arguments);
};
$.fn.hasChanged = function (types, data, fn) {
return this.on(fctsObserveKeys, types, null, data, fn);
};
$.extend($, {
observeMethods: function (namespace) {
var namespace = namespace ? "." + namespace : "";
var _len = $.fn.length;
delete $.fn.length;
$.each(fctsToObserve, function (key) {
var _pre = this;
$.fn[key] = function () {
var target = _pre[1] === 'self' ? this : this.parent(),
ret = _pre[0].apply(this, arguments);
target.trigger("hasChanged." + key + namespace, arguments);
return ret;
};
});
$.fn.length = _len;
}
});
$.observeMethods()
})(jQuery);
I know that having the value of this being changed to the element receiving the event in event handling functions is pretty useful. However, I'd like to make my functions always be called in my application context, and not in an element context. This way, I can use them as event handlers and in other ways such as in setTimeout calls.
So, code like this:
window.app = (function () {
var that = {
millerTime: function () {},
changeEl: function (el) {
el = el || this;
// rest of code...
that.millerTime();
}
};
return that;
}());
could just be like this:
window.app = (function () {
return {
millerTime: function () {},
changeEl: function (el) {
// rest of code...
this.millerTime();
}
};
}());
The first way just looks confusing to me. Is there a good easy way to pass the element receiving the event as the first argument (preferably a jQuery-wrapped element) to my event handling function and call within the context of app? Let's say I bind a bunch of event handlers using jQuery. I don't want to have to include anonymous functions all the time:
$('body').on('click', function (event) {
app.changeEl.call(app, $(this), event); // would be nice to get event too
});
I need a single function that will take care of this all for me. At this point I feel like there's no getting around passing an anonymous function, but I just want to see if someone might have a solution.
My attempt at it:
function overrideContext (event, fn) {
if (!(this instanceof HTMLElement) ||
typeof event === 'undefined'
) {
return overrideContext;
}
// at this point we know jQuery called this function // ??
var el = $(this);
fn.call(app, el, event);
}
$('body').on('click', overrideContext(undefined, app.changeEl));
Using Function.prototype.bind (which I am new to), I still can't get the element:
window.app = (function () {
return {
millerTime: function () {},
changeEl: function (el) {
// rest of code...
console.log(this); // app
this.millerTime();
}
};
}());
function overrideContext (evt, fn) {
var el = $(this); // $(Window)
console.log(arguments); // [undefined, app.changeEl, p.Event]
fn.call(app, el, event);
}
$('body').on('click', overrideContext.bind(null, undefined, app.changeEl));
Using $('body').on('click', overrideContext.bind(app.changeEl)); instead, this points to my app.changeEl function and my arguments length is 1 and contains only p.Event. I still can't get the element in either instance.
Defining a function like this should give you what you want:
function wrap(func) {
// Return the function which is passed to `on()`, which does the hard work.
return function () {
// This gets called when the event is fired. Call the handler
// specified, with it's context set to `window.app`, and pass
// the jQuery element (`$(this)`) as it's first parameter.
func.call(window.app, $(this) /*, other parameters (e?)*/);
}
}
You'd then use it like so;
$('body').on('click', wrap(app.changeEl));
For more info, see Function.call()
Additionally, I'd like to recommend against this approach. Well versed JavaScript programmers expect the context to change in timeouts and event handlers. Taking this fundamental away from them is like me dropping you in the Sahara with no compass.
How can I do that?
It seems that you can have multiple jQuery's ready() functions, and they will all run when the DOM is loaded.
So how can I create my own ready()-like function? :)
function _addEvent(e, evt, handler){
if(evt == "ready")
evt = "DOMContentLoaded";
if(typeof handler !== 'function')return;
if (e.addEventListener)
e.addEventListener(evt, handler, false);
else if (e.attachEvent)
e.attachEvent("on" + evt, handler);
else
{
var oldHandler = e["on" + evt];
function newHandler(event){
handler.call(e, event);
if(typeof oldhandler === 'function')oldhandler.call(e, event);
}
}
}
var _events = ["ready", "click", "mousedown"]; //...
var _myLib = function(item){
function eventWorker(item, event){
this.add = function(handler){
_addEvent(item, event, handler);
};
}
for(var i=0;i<_events.length;i++)
this[_events[i]] = (new eventWorker(item, _events[i])).add;
};
var MyLib = function(item){
return new _myLib(item);
};
MyLib(document).ready(function(){alert("I am ready!");});
Test =>
http://jsfiddle.net/vgraN/
First, you need to identify what it is you need the function for - is it to respond to a particular browser event?
jQuery's $(document).ready(fn) uses an array internally to hold the functions to execute when the DOM has loaded. Adding a new ready(fn) call appends the function fn to the array. When the DOM has loaded (which is detected in various ways according to which browser the code is executing within), each function in turn in the array is executed. Any functions added using ready(fn) after the DOM has loaded are executed immediately.
In summary, you can use an array to store the functions to execute whenever it is that you need to execute them.
Take a look at domready, a standalone port of the ready(fn) function from jQuery to get some ideas about how to go about it.
It sounds like you want to make an array of functions and append new callbacks to it.
It's not easy to do cross browser.
If you assume the DOMContentLoaded event exists then you can just make
var domready = (function () {
var cbs = [];
document.addEventListener("DOMContentLoaded", function () {
cbs.forEach(function (f) {
f();
});
});
return function (cb) {
cbs.push(cb);
};
})();
You can use other fallbacks like window.onload and a hackish scroll check like jQuery does.
I'd recommend either using domready or reading the source.
Do you want to create a function which when passed a function will call that function at a particular time? (Also, it can be called multiple times.) If so this is how I would do it it. (Based on jQuery code.)
var funcQueue = (function () {
var funcList = [];
function runAll() {
var len = funcList.length,
index = 0;
for (; index < len; index++)
funcList[index].call(); // you can pass in a "this" parameter here.
}
function add(inFunc) {
funcList.push(inFunc);
}
})();
To use:
funcQueue.add(function () { alert("one"); });
funcQueue.add(function () { alert("two"); });
funcQueue.runAll(); // should alert twice.
so is something like this possible?
Y.one("input.units").on("keyup change", function(e){
...
});
the jquery equivalent is
$("input.units").bind("keyup change", function(e){
...
});
Yes, this is possible. Just pass an array of event names instead of a string:
Y.one('input.units').on(['keyup', 'change'], function (e) {
// ...
});
Why not try something like this:
var actionFunction = function(e) { /* stuff goes here */ };
node.one("input.units").on("keyup", actionFunction);
node.one("input.units").on("change", actionFunction);
EDIT: YUI supports this natively. See Ryan's answer below.
No. You could do something like this, though:
YUI().use("node", "oop", function (Y) {
var on = Y.Node.prototype.on;
function detachOne(handle) {
handle.detach();
}
Y.mix(Y.Node.prototype, {
on: function (type, fn, context) {
var args = Y.Array(arguments),
types = args[0].split(" "),
handles = [];
Y.each(types, function (type) {
args[0] = type;
handles.push(on.apply(this, args));
})
return {
detach: Y.bind(Y.each, null, handles, detachOne)
};
}
}, true);
})
This code wraps Node.on() to accept a string of space-delimited event types. It returns an object with a single method, detach, which detaches your handler from all of the events.
Note that this code only affects the Y instance inside its sandbox, so you should put it inside the function that you pass to YUI().use. It would also be easy to package it up as a module.