jQuery - why can't I bind events to elements in a loop? - javascript

Here is my code:
var b = $(slipStream.conf.mainVis).find('p#prev');
b.click(function() {
slipStream.slideLeft();
return false;
});
b = $(slipStream.conf.mainVis).find('p#next');
b.click(function() {
slipStream.slideRight();
return false;
});
b = $(slipStream.conf.controls).find('li img');
console.log(b);
for (var l in b) {
l.click(function() {
var visIndex = l.index();
console.log(visIndex);
});
};
The first two bindings go through, no problem. But I can't loop through a collection and bind something to each member? (the console is telling me that "l.click is not a function.") Is this a limitation of jQuery or is my code off? This seems like it would be the way to do it, though...

When you enumerate over a jQuery object, the values being enumerated are actual DOM nodes and not jQuery wrappers. Therefore, they don't have a click method but you can wrap them again to get all the usual functionality.
Of course this is not necessary because you can simply attach a wrapper directly from your initial jQuery instance:
$(slipStream.conf.controls).find('li img').click(function() {
var visIndex = $(this).index();
console.log(visIndex);
});

This is the classic "loop variables don't work properly in callbacks" bug.
Your variable l no longer has the originally supplied value by the time the callback is invoked - it has whatever final value was assigned in the last pass through the loop.
[FWIW, l isn't actually a jQuery object, so you have to wrap it - $(l) to use it with jQuery]
The usual fix to the loop bug is to create an additional closure that returns a function bound to the current value:
for (var l in b) { // NB: don't use `for ... in ...` on array-like objects!
var make_cb = function(n) {
return function() {
var visIndex = $(n).index();
console.log(visIndex);
}
}
$(l).click(make_cb(l));
};
Fortunately, you don't need a loop at all - you can have jQuery automatically add the callback to every element by itself:
b = $(slipStream.conf.controls).find('li img');
b.click(function() {
var visIndex = $(this).index();
console.log(visIndex);
});

Could it be that the problem is the forloop. .click is part of the jQuery, so you must be sure that it's called on element that is wrapper with jQuery.
$.each(b, function (index, element) {
$(element).click(function() {
});
};

With each() you can iterate through a set of jQuery objects:
$(slipStream.conf.controls).find('li img').each(function(){
$(this).click(function() {
var visIndex = $(this).index();
console.log(visIndex);
});
});
$(this) will match the currently indexed object from the collection.

Related

Manipulating a jquery collection

I have a JQuery collection created with:
var children = $('span[data-Parent="7"]');
Now I would like to manipulate the css class of the items in that collection. I've tried:
for (var i = 0; i < children.length; i++) {
var $child = children[i];
if ($child.hasClass("red")) {
$child.addClass("green");
}
But get errors stating that the object doesn't have a method hasClass. What is the proper way to get the elements of the collection to be document elements so they can be manipulated?
You must wrap the DOM element as a jQuery object. And iterating over elements using jQuery is usually done with the provided each function :
$('span[data-Parent="7"]').each(function(){
if ($(this).hasClass('red')) {
$(this).addClass('green');
}
});
Note that the whole code can be reduced to
$('span[data-Parent="7"].red').addClass('green');
I'd say use $.each:
$.each(children, function(i, child) {
if ($(child).hasClass("red")) {
$(child).addClass("green");
}
});
Just cycle through each of the children and run the check in the .each loop.
var children = $('span[data-Parent="7"]');
children.each(function(index)
{
var self = $(this);
if (self.hasClass("red"))
{
self.addClass("green");
}
});
Change your code to:
var $child = $(children[i]);
//the rest of the code

Assign a Function Argument with a Loop

I have an array of list items in a piece of Javascript code. I would like to assign an onclick event handler to each one. Each handler would be the same function, but with a different input argument. Right now I have:
function contentfill(i) {
box = document.getElementById("text");
box.style.background="rgba(0,0,0,0.8)";
var content = new Array();
contentdivs = document.querySelectorAll("#contentfill>div");
box.innerHTML = contentdivs[i].innerHTML;
}
li[3].onclick = function() {contentfill(0);};
li[4].onclick = function() {contentfill(1);};
li[5].onclick = function() {contentfill(2);};
This works well enough, but I would like to achieve the same thing with a loop, for example:
for(i=3;i<=5;i++) {
j=i-3;
li[i].onclick = function() {contentfill(j);};
}
This, however, does not work. Since j seems to be defined as 2 at the end of the loop, each time I click, it only seems to call contentfill(2).
For an alternative approach, consider having each of the elements aware of what argument it should be using.
for (var i = 0; i < 3; i++) {
var el = li[i + 3];
el.dataset.contentIndex = i;
el.addEventListener('click', contentfill);
}
Then contentfill would have to extract the argument from .dataset instead of taking an argument, of course. (This is the same mechanism as jQuery's $.data.)
I tend to prefer this since (a) it doesn't generate tons of tiny wrappers, (b) it allows me to later examine and possibly change the "arguments", and (c) it lets me predefine them in the document using data- attributes. Effectively changes them from function arguments into behavior.
The value of i - 3 should be bound to the click handler function; a closure can provide this functionality:
li[i].onclick = (function(j) {
return function() {
contentfill(j);
}
)(i - 3));
Btw, it's better practice to use addEventListener or attachEvent to register click handlers.

How can I refresh a stored and snapshotted jquery selector variable

I ran yesterday in a problem with a jquery-selector I assigned to a variable and it's driving me mad.
Here is a jsfiddle with testcase:
assign the .elem to my obj var
log both lengths to the console. Result => 4
Remove #3 from the DOM
log obj to the console => the removed #3 is still there and the length is still 4.
I figured out that jquery query is snapshotted? to the variable and can't?won't? be updated
log .elem to the console.. yep Result => 3 and the #3 is gone
Now I update .elem with a new width of 300
logging obj & obj.width gives me 300.. So the snapshot has been updated ? What's interesting is that 3 of the 4 divs have the new width, but the removed #3 doesn't...
Another test: Adding a li element to the domtree and logging obj and .elem.
.elem does have the new li and obj doesn't, because it's still the old snapshot
http://jsfiddle.net/CBDUK/1/
Is there no way to update this obj with the new content?
I don't want to make a new obj, because in my application there is a lot information saved in that object, I don't want to destroy...
Yeah, it's a snapshot. Furthermore, removing an element from the page DOM tree isn't magically going to vanish all references to the element.
You can refresh it like so:
var a = $(".elem");
a = $(a.selector);
Mini-plugin:
$.fn.refresh = function() {
return $(this.selector);
};
var a = $(".elem");
a = a.refresh();
This simple solution doesn't work with complex traversals though. You are going to have to make a parser for the .selector property to refresh the snapshot for those.
The format is like:
$("body").find("div").next(".sibling").prevAll().siblings().selector
//"body div.next(.sibling).prevAll().siblings()"
In-place mini-plugin:
$.fn.refresh = function() {
var elems = $(this.selector);
this.splice(0, this.length);
this.push.apply( this, elems );
return this;
};
var a = $(".elem");
a.refresh() //No assignment necessary
I also liked #Esailija solution, but seems that this.selector has some bugs with filter.
So I modified to my needs, maybe it will be useful to someone
This was for jQuery 1.7.2 didn`t test refresh on filtered snapshots on higher versions
$.fn.refresh = function() { // refresh seletor
var m = this.selector.match(/\.filter\([.\S+\d?(\,\s2)]*\)/); // catch filter string
var elems = null;
if (m != null) { // if no filter, then do the evarage workflow
var filter = m[0].match(/\([.\S+\d?(\,\s2)]*\)/)[0].replace(/[\(\)']+/g,'');
this.selector = this.selector.replace(m[0],''); // remove filter from selector
elems = $(this.selector).filter(filter); // enable filter for it
} else {
elems = $(this.selector);
}
this.splice(0, this.length);
this.push.apply( this, elems );
return this;
};
Code is not so beautiful, but it worked for my filtered selectors.
Clean and generic solution worked properly with jQuery 3.4.1:
My solution is to do the following:
Intercept the selector at the time of jQuery object initialization and in the same time maintain all other jQuery functionalities transparently all this using inheritance
Build refresh plugin that make use of the new "selector" property we added during initialization
Definition:
$ = (function (originalJQuery)
{
return (function ()
{
var newJQuery = originalJQuery.apply(this, arguments);
newJQuery.selector = arguments.length > 0 ? arguments[0] : null;
return newJQuery;
});
})($);
$.fn = $.prototype = jQuery.fn;
$.fn.refresh = function ()
{
if (this.selector != null && (typeof this.selector === 'string' || this.selector instanceof String))
{
var elems = $(this.selector);
this.splice(0, this.length);
this.push.apply(this, elems);
}
return this;
};
Usage:
var myAnchors = $('p > a');
//Manipulate your DOM and make changes to be captured by the refresh plugin....
myAnchors.refresh();
//Now, myAnchors variable will hold a fresh snapshot
Note:
As optimization, object selectors don't need refresh as they are pass by reference by nature so, in refresh plugin, we only refresh if the selector is a string selector not object selector for clarification, consider the following code:
// Define a plain object
var foo = { foo: "bar", hello: "world" };
// Pass it to the jQuery function
var $foo = $( foo );
// Test accessing property values
var test1 = $foo.prop( "foo" ); // bar
// Change the original object
foo.foo = "koko";
// Test updated property value
var test2 = $foo.prop( "foo" ); // koko
Jquery .selector is deprecated, it's better to remeber string with selector value to some variable at the moment when you assign
function someModule($selector, selectorText) {
var $moduleSelector = $selector;
var moduleSelectorText = selectorText;
var onSelectorRefresh = function() {
$moduleSelector = $(moduleSelectorText);
}
}
https://api.jquery.com/selector/
You can also return the JQuery selector in a function, and save this function into the variable. Your code will look a bit different but it works. Every time when you execute the function, your jquery selector will search the DOM again.
In this example I used an arrow function without brackets which will return whatever is next to arrow. In this case it will return the JQuery collection.
const $mySelector = () => $('.selector');
console.log($mySelector().last().text());
$('.parent').append('<li class="selector">4</li>')
console.log($mySelector().last().text()); //RETURNS 4 not 3
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<ul class="parent">
<li class="selector">1</li>
<li class="selector">2</li>
<li class="selector">3</li>
</ul>
If you use remove() it will remove only a part of the DOM but not all the children or related, instead if you use empty() on the element the problem is gone.
E.G.:
$('#parent .child).find('#foo').empty();
Maybe it can be useful to someone!

Question regarding "this" inside anonymous function of prototype.js .each method

I've been searching for hours for a solution to this problem. I'm creating a table using prototype.js 1.6.0.1 and am having trouble with the this object in context with the .each function. here is a snippit.
var Table = Class.create({
initialize : function(id) {
this.elmnt = $(id);
this.rows = [];
},
initRows : function() {
$A(this._elmnt.tBodies).each(function(body) {
$A(body.rows).each(function(row) {
//right here is where i would like to call
// this.rows.push(row);
console.log(this); // prints DOMWindow
});
});
}
});
As you can see inside the second .each function this resolves to DOMWindow. I would like to be able to call this.rows.push(row) but I can't as "this" isn't resolving as expected.
Any help would be appreciated. I know i could do the standard (i=0; i < length; i++) loop but I was trying to make this a little cleaner. Thanks for any guidance you can offer.
The easiest way to work around this is to save this at the start of initRows and refer to in within the each functions
initRows : function() {
var self = this;
$A(this._elmnt.tBodies).each(function(body) {
$A(body.rows).each(function(row) {
//right here is where i would like to call
self.rows.push(row);
console.log(self); // prints DOMWindow
});
});
}
The problem you're running into is that this can be manipulated by the caller of the function. It's very common in callbacks to set this to an element which is relevant to the call back. In the case of each it's set to the element for the current iteration of the value.
The self trick works because it saves the this as it's bound in the function initRows and then uses that saved value in the iteration.
initRows : function() {
$A(this._elmnt.tBodies).each(function(body) {
$A(body.rows).each((function(e, row) {
e.rows.push(row);
console.log(e);
}).bindAsEventListener(this, row));
});
}

remove elements from the jQuery object from within a plugin

I think I have mistaken some fundamentals here, because I think this should work. I am trying to to through the child p and div elements of the matched set, and remove those which fail to meet the required wordcount from the matched set.
I have tested the wordCount plugin, and the if statement it is being used it, and all seems to be working fine, but my element is not being removed from the matched set.
(function($){
$.fn.extend({
textBlocks: function(count){
var JQ_Object = $(this);
if(!count) count = 100;
return this.each(function(){
$(this).find("p, div").each(function(){
if($(this).wordCount()<count){
var x = $(this);
JQ_Object.not(x);
};
});
return JQ_Object;
});
}
});
})(jQuery);
Here is the wordCount plugin, just in case you wondered:
(function($){
$.fn.extend({
wordCount: function(){
return $(this).html().split(" ").length;
}
});
})(jQuery);
I made a few changes... see fiddle for working example and code for comments.
http://jsfiddle.net/8PXpt/
(function ($){
$.fn.extend({
wordCount: function (){
//Don't need $(this), this already refers to the jQuery object
//Always trim .html() and .text() when using .split()
//May want to use .text() instead of .html() - I leave that to you
return $.trim(this.html()).split(' ').length;
}
});
})(jQuery);
(function ($){
$.fn.extend({
textBlocks: function (count){
var collection = this;
//Check that a number was passed
//"50" would break your extension
if(typeof count !== 'number') {
count = 100;
}
//Removed $('div, p') - this should be part of your call
//See ready function below
this.each(function (){
if ($(this).wordCount() < count){
//This could be double assignment
//but better safe than sorry
collection = collection.not(this);
};
});
//Return what is left (what passed)
return collection ;
}
});
})(jQuery);
$(function() {
//Here is where you define your selector... in your case 'div, p'
$('div, p').textBlocks(2);
});
Have you tried $(this).remove() rather than JQ_Object.not(x);
I think .not() removes them from the selection rather than from the HTML... unless that's what you're trying to do
You're creating a new JQ_Object in the internal each, so I'm not sure if it would modify the original JQ_Object. I'm not 100% on that though. Try JQ_Object.not(this).
This assumes, however, that .each is synchronous, which I'd hope it isn't. If that's the case, you'd need to make use of jQuery's while function.
This should give you the desired result, but I'd be wary each being asynchronous.
return $(this).find("p, div").each(function(){
if($(this).wordCount()<count){
JQ_Object.not(this);
};
});
EDIT:
I'm not to sure about the above code. What I'd do is use a callback. This assumes a callback is passed in to your plugin.
$(this).find("p, div").each(function(){
if($(this).wordCount()<count){
JQ_Object.not(this);
};
}).when(function () {
callback(JQ_Object);
});

Categories

Resources