This is a fragment of a plugin that I'm using on my site:
$.fn.extend({
limiter: function(limit, elem) {
$(this).on("keyup focus", function() {
setCount(this, elem);
});
function setCount(src, elem) {
...
}
}
});
The setCount() function works well only if the elem property in not an array - only single values are accepted.
I'd like to change it so that I can pass arrays (multiple selection in my case) to the setCount() function.
I thought that a solution would be to use jQuery.each() iterator like so:
$.fn.extend({
limiter: function(limit, elem) {
$(this).on("keyup focus", function() {
$.each(elem, setCount)
});
function setCount(src, elem) {
...
}
}
});
The problem is that I also have to pass the this object which points to the object that received "keyup focus" as is the case in the first snippet.
How do I pass this object to the callback property of $.each() in the given scenario?
Try binding the context, using bind (warning not IE8 compatible):
$.each(elem, setCount.bind(this))
This makes the this-context inside each setCount-call, pointing to this from the event handler.
You can keep a reference to the "first" this and use it in setCount:
$.fn.extend({
limiter: function(limit, elem) {
var that = this;
$(this).on("keyup focus", function() {
$.each(elem, setCount)
});
function setCount(src, elem) {
// use 'that' here
...
}
}
});
Related
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.
Sometimes I run into situations where I'm having to create the same variables, and retrieve the exact same type of information over & over again while inside of a object literal, such as an .on() Out of sheer curiosity, and the fact that there has to be a better way, here I am.
NOTE I am not talking about jQuery .data() or any sort of normal window. global variable. I am talking one that is maintained within the closure of the object literal.
Some of these variables change in real-time of course, hence why I always had them inside of each method within .on()
Case in point:
$(document).on({
focusin: function () {
var placeHolder = $(this).attr('data-title'),
status = $(this).attr('data-status');
// etc etc
},
focusout: function () {
var placeHolder = $(this).attr('data-title'),
status = $(this).attr('data-status');
// etc etc
},
mouseenter: function () {
// same variables
},
mouseleave: function () { }
}, '.selector');
Is there a way to just have the variables stored somewhere, and retrieve on each event? They need to be dynamic
$(document).on({
// Basially:
// var placeHolder; etc
// each event gets these values
focusin: function () {
// now here I can simply use them
if (placeHolder === 'blahblah') {}
// etc
}
}, '.selector');
You can use event data to pass some static variables to the event, as well as to make a method-wise trick to pass the "dynamic" ones:
$(document).on({
focusin: function(e) {
var attrs = e.data.getAttributes($(this)),
var1 = e.data.var1;
var placeHolder = attrs.title,
status = attrs.status;
// ...
},
focusout: function(e) {
var attrs = e.data.getAttributes($(this)),
var1 = e.data.var1;
var placeHolder = attrs.title,
status = attrs.status;
// ...
},
// ...
}, ".selector", {
// static variables
var1: "some static value",
// dynamic "variables"
getAttributes: function($this) {
return {
title: $this.data("title"),
status: $this.data("status")
};
}
});
DEMO: http://jsfiddle.net/LHPLJ/
There are a few ways:
Write your own function that will return the JSON above; you can loop through properties to keep from duplicating work.
Write a function that returns those variables (eg: as JSON) so you need only call one function each time.
Write a function to set those variables as global properties and refer to them as needed.
Why not simply add a helper function to extract it?
var getData = function(elm) {
return {
placeHolder : $(elm).attr('data-title'),
status : $(elm).attr('data-status');
};
};
$(document).on({
focusin: function () {
var data = getData (this);
// do stuff with data.status etc.
},
//repeat...
If you're always targeting the same element (i.e. if there's a single element with class selector), and if the values of those variables won't change between the different times the events are triggered, you can store them on the same scope where the handlers are defined:
var placeHolder = $('.selector').attr('data-title'),
status = $('.selector').attr('data-status');
$(document).on({
focusin: function () {
// etc etc
},
focusout: function () {
// etc etc
},
mouseenter: function () {
// same variables
},
mouseleave: function () { }
}, '.selector');
Functions declared on the same scope as those variables will have access to them.
I would recommend wrapping everything in a function scope so that the variables are not global. If these attributes never change, you could do something like:
(function(sel){
var placeHolder = $(sel).attr('data-title'),
status = $(sel).attr('data-status');
$(document).on({
focusin: function () {
// etc etc
},
focusout: function () {
// etc etc
},
mouseenter: function () {
// same variables
},
mouseleave: function () { }
}, sel);
})('.selector');
Otherwise, you could do this (in modern browsers, IE9+)
(function(sel){
var obj = {};
Object.defineProperty(obj, 'placeHolder', {
get:function(){return $(sel).attr('data-title');}
});
Object.defineProperty(obj, 'status', {
get:function(){return $(sel).attr('data-status');}
});
$(document).on({
focusin: function () {
console.log(obj.placeHolder, obj.status);
//etc etc
},
focusout: function () {
// etc etc
},
mouseenter: function () {
// same variables
},
mouseleave: function () { }
}, sel);
})('.selector');
You're doing it close to correct, but the best way to do it is this:
$(document).on({
focusin: function () {
var el = $(this), //cache the jQ object
placeHolder = el.data('title'),
status = el.data('status');
// etc etc
}
}, '.selector');
Data was created for this purpose, don't worry about trying to create re-useable items. If you're delegating the event using object, then it's probably because the elements aren't always on the page in which case you need to get the variables within each individual event.
Finally, don't try to optimize when you don't need to.
You can save them in the window object and make them global.
window.yourVariable = "whatever";
This does what you want but is for sure not the most clean way. If you can, you can consider saving the desired data to the object itself via $(element).data("key", "value")
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);
};
If I'm using the following function :
clusters.prototype.shop_iqns_selected_class = function() {
if(this.viewport_width < 980) {
$(this.iqns_class).each(function() {
$(this.iqn).on('click', function() {
if($(this).hasClass('selected')) {
$(this).removeClass('selected');
} else {
$(this).addClass('selected');
}
});
});
}
}
To add a property to the clusters function, I know that using this.viewport_width I'm referring to the parent function where I have this.viewport_width defined, but when I'm using the jQuery selector $(this), am I referring to the parent of the $.on() function ?
In JavaScript, this is defined entirely by how a function is called. jQuery's each function calls the iterator function you give it in a way that sets this to each element value, so within that iterator function, this no longer refers to what it referred to in the rest of that code.
This is easily fixed with a variable in the closure's context:
clusters.prototype.shop_iqns_selected_class = function() {
var self = this; // <=== The variable
if(this.viewport_width < 980) {
$(this.iqns_class).each(function() {
// Do this *once*, you don't want to call $() repeatedly
var $elm = $(this);
// v---- using `self` to refer to the instance
$(self.iqn).on('click', function() {
// v---- using $elm
if($elm.hasClass('selected')) {
$elm.removeClass('selected');
} else {
$elm.addClass('selected');
}
});
});
}
}
There I've continued to use this to refer to each DOM element, but you could accept the arguments to the iterator function so there's no ambiguity:
clusters.prototype.shop_iqns_selected_class = function() {
var self = this; // <=== The variable
if(this.viewport_width < 980) {
// Accepting the args -----------v -----v
$(this.iqns_class).each(function(index, elm) {
// Do this *once*, you don't want to call $() repeatedly
var $elm = $(elm);
// v---- using `self` to refer to the instance
$(self.iqn).on('click', function() {
// v---- using $elm
if($elm.hasClass('selected')) {
$elm.removeClass('selected');
} else {
$elm.addClass('selected');
}
});
});
}
}
More reading (posts in my blog about this in JavaScript):
Mythical methods
You must remember this
Don't use this all throughout the code. Methods like $.each give you another reference:
$(".foo").each(function(index, element){
/* 'element' is better to use than 'this'
from here on out. Don't overwrite it. */
});
Additionally, $.on provides the same via the event object:
$(".foo").on("click", function(event) {
/* 'event.target' is better to use than
'this' from here on out. */
});
When your nesting runs deep, there's far too much ambiguity to use this. Of course another method you'll find in active use is to create an alias of that, which is equal to this, directly inside a callback:
$(".foo").on("click", function(){
var that = this;
/* You can now refer to `that` as you nest,
but be sure not to write over that var. */
});
I prefer using the values provided by jQuery in the arguments, or the event object.
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.