JQuery: Elements made with clone() not taking event properties of original - javascript

HTML:
<h2>CHECK AS MANY AS YOU CAN</h2>
<form id="boxone">
</form>
JS:
$boxone = $("#boxone");
$boxone.html('<input type="checkbox" class="fourthboxes">');
$fourthboxes = $(".fourthboxes");
for(var i=0; i <341; i++) {
$fourthboxes.clone(true, true).appendTo($boxone);
}
$fourthboxes.change(function() {
alert('yo');
});
The rest of the checkboxes don't alert when I click on them, only the original one does
I even tried $fourthboxes.on('click'... instead and still nothing.
I took a look at this question and tried the solution but it didn't work.
jQuery clone() not cloning event bindings, even with on()

Use .on():
$(document).on('change', '.fourthboxes', function() {
alert('yo');
});
This makes your event handler work for current elements, but also future added elements, that match the .fourthboxes selector. This uses the principle of delegated events.
From the documentation:
When a selector is provided, the event handler is referred to as delegated. The handler is not called when the event occurs directly on the bound element, but only for descendants (inner elements) that match the selector. jQuery bubbles the event from the event target up to the element where the handler is attached (i.e., innermost to outermost element) and runs the handler for any elements along that path matching the selector.
Delegated events have the advantage that they can process events from descendant elements that are added to the document at a later time.

The issue is because although you use clone(true, true) you're cloning the elements before you add the change event handler to them. You just need to swap the logic around:
$boxone = $("#boxone");
$boxone.html('<input type="checkbox" class="fourthboxes">');
$fourthboxes = $(".fourthboxes");
$fourthboxes.change(function() {
alert('yo');
});
for(var i=0; i <341; i++) {
$fourthboxes.clone(true, true).appendTo($boxone);
}
However, it would be much better to use a single delegated event handler, like this:
var $boxone = $("#boxone").on('change', '.fourthboxes', function() {
alert('yo');
});
var $fourthboxes = $('<input type="checkbox" class="fourthboxes">').appendTo('#boxone');
for (var i = 0; i < 341; i++) {
$fourthboxes.clone().appendTo($boxone);
}
Working example

Since you assign $(".fourthboxes") to the variable $fourthboxes before you add the 340 other checkboxes, the variable still holds only that one checkbox when you add the change-function.
Put the change-function in front of the for-loop and everything works as expected.

It only selects the original because you never select the new elements. The selector only has the original, it is not a live collection. So you need to reselect them.
$(".fourthboxes").on("change", ...)
You can use event delegation so you are not selecting all the checkboxes. Listen for the change event on the form.
$("#boxone").on("change", ".fourthboxes", function(){});

When you clone an object, only the objects are cloned. And not their events. Because the events are bound to the original object (based on the jQuery selector used) before creating the clone.
As #trincot mentioned in his answer, you need to have an event at the document level.
For eg. Let's say my DOM contains three input checkboxes
<input type="checkbox" class="fourthboxes">
<input type="checkbox" class="fourthboxes">
<input type="checkbox" class="fourthboxes">
Now when you bind events using the jQuery selector like this,
$(".fourthboxes").change(function() {
alert('yo');
});
Things to note, is that this jQuery selector returns an array of DOM elements which are present on the page, at that instant of time. And then the onchange event is registered on each of them. It is equivalent to binding the event to each of the existing DOM.(In this case, three checkboxes)

Related

With pure JavaScript, is there an easy way to attach an event listener to a class?

I'm looking for the JavaScript equivalent of
$(".class-name").on(eventName, eventHandler);
first I tried
document.querySelectorAll(".class-name").addEventListener(eventName, eventHandler);
but it didn't work. Is there an easy way to do it in JavaScript?
------Update------
Many answers suggested a loop, but originally I was looking for a solution with a single listener to save memory (suppose I had 1000 items in that class).
However, one answer reminded me: does the jQuery version also create 1000 listeners in the memory?
jQuery will assign the event handler passed to on to all the elements that match the selector. Vanilla JS does not do this. document.querySelectorAll returns a NodeList and so you'll need to select the specific node you want and assign the event listeners to it. If you want to assign the event listener to all the nodes in the list, then you may iterate over the NodeList like so:
var nodes = document.querySelectorAll(".class-name");
for (var node in nodes) {
node.addEventListener(event, handler);
}
The issues is querySelectorAll returns a nodeList, if you only looked for one class using querySelector, your code would work, but only bind to one element.
Option 1: Iterate over NodeList + Bind
One option is to iterate over all the nodes in the nodelist and bind the event to them:
let eventName = 'click';
let elements = document.querySelectorAll('.foo');
[...elements].forEach(el=>el.addEventListener(eventName, eventHandler));
function eventHandler (){
console.log(`Clicked: "${this.textContent}"`);
}
<div class="foo">Click Me</div>
<div class="foo">Or this</div>
Note: the above uses ES6 syntax, if you need an iteration method that is a little more compliant, you can try:
[].forEach.call(elements, function(el) { ... });
Option 2: Bind Once to Common Ancestor
Another option would be to find the closest common ancestor and bind one event to that, then see if the class you care about is the target and call the function, if so.
You could loop over document.getElementsByClassName('this-is-the-class-you-are-searching-for') like
document.getElementsByClassName('selector-class').forEach(function (e) {
e.addEventListener('click', function (event) {
console.log('Oh dang, you just clicked!');
});
});
As Alex M. pointed out, the NodeList.forEach is not supported by some browsers such as InternetExplorer. So, you could use a different syntax (for) or use a transpiler like babel to ensure the wide support of your code! The polyfill from MDN is also very small.
Here's the equivalent (this will also work for future nodes)
document.addEventListener("click", function(e) {
var el = e.target;
if (el.classList.contains("class-name")) {
// handle event e for element el
}
});
Edit: while the same is possible in jQuery, this way is actually superior to the example from the Question. This listener will also catch clicks on elements that didn't exist when the listener was added.
Store all elements in array and bind event to all of them. Here document.getElementsByClassName returns array of element having class class-name.
var classes= document.getElementsByClassName("class-name");
Array.from(classes).forEach(function(element) {
element.addEventListener('click', function(){alert('clicked'+ this.innerHTML)});
});
<div class="class-name">
First div 1
</div>
<div class="class-name">
Second div 2
</div>
<div class="class-name">
third div 3
</div>
.
.
.
<div class="class-name">
nth div n
</div>
Edit
getElementsByClassName doesnt return an array, but a HTMLCollection in most, or a NodeList is some browsers (Mozila ref). Both of these types are Array-Like, (meaning that they have a length property and the objects can be accessed via their index), but are not strictly an Array or inherited from an Array. (meaning other methods that can be performed on an Array cannot be performed on these types)
Yes, absolutely!
EDIT: Turns out you want to add it to every element with that class. In that case you should try with
let elements = document.getElementsByClassName("myClass")
After that you could just cycle through them with a for loop and attach the event listener.

How do I make the checkbox work on newly created task?

I am writing a todo app using vanilla JavaScript to learn the language without using a library.
In the app, you can add a task, complete task or un-complete task, edit and delete task. I have sample tasks that show the functionality in HTML.
I have a function that adds a task to incomplete tasks section—each task item is wrapped in an li tag and has a checkbox, edit and delete buttons. The addTask function works perfectly.
The problem I am having is in the selectbox part. The app is designed in a way that when checkbox of a task is selected, it indicates a task is completed and thus the item is shown in the completed tasks section. The function works for the items available on the page but not the ones added using the add functionality. How do I make the new task work? Thanks. Here is my HTML Code:
<h3>Todo</h3>
<ul id="incomplete-tasks">
<li><input type="checkbox"><label>Pay Bills</label><button>Edit</button><button>Delete</button></li>
</ul>
<h3>Completed</h3>
<ul id="completed-tasks">
<li><input type="checkbox" checked></input><label>See Doctor</label><button>Edit</button><button>Delete</button></li>
And here is my JS code:
var incompleteTasksHolder = document.getElementById("incomplete-tasks");
var completedTasksHolder = document.getElementById("competed-tasks");
var incompleteTextboxes = incompleteTasksHolder.querySelectorAll("input[type=checkbox]");
for (var i = 0; i < incompleteTextboxes.length; i++) {
incompleteTextboxes[i].onfocus = function() {
var item = this.parentNode;
this.setAttribute("checked", "checked");
completedTasksHolder.appendChild(item);
}
}
You are probably using some event listeners to mark the task as completed when the checkbox changes value.
I think that you just forgot the bind this listener to the new item that you created via Javascript.
When creating your new element bind your listener to it:
taskCB.addEventListener('change', toggleCompleted);
Where taskCB is the checkbox element and toggleCompleted is the name of the function that is fired when a task checkbox is checked or unchecked.
This probably happens because the incompleteTextboxes variable only gets assigned a collection of DOM nodes once. Each time you add a new task, that ends up inside incompleteTasksHolder you should also re-assign an updated collection by re-running the assignment: incompleteTextboxes = incompleteTasksHolder.querySelectorAll("input[type=checkbox]");.
That could look something like this:
var incompleteTasksHolder = document.getElementById("incomplete-tasks"),
completedTasksHolder = document.getElementById("competed-tasks");
// Initialize variable, but create a function for the assignment:
var incompleteTextboxes;
// This function searches the DOM for all checkboxes inside incompleteTasksHolder, and assigns the collection of found elements to incompleteTextboxes:
function refreshIncompleteTasksCollection() {
incompleteTextboxes = incompleteTasksHolder.querySelectorAll("input[type=checkbox]");
}
// You can now re-assign all currently existing checkboxes by calling this function:
refreshIncompleteTasksCollection();
Now, each time there are new elements inside incompletetextboxes, you can run that function to add those to your collection. Note, you'll probably want to re-run your for-loop as well, for binding the focus event handler to your new elements.
A whole different (and in my opinion cleaner) approach would be to use event delegation for your onfocus handlers. Since incompletetextboxes only contains checkboxes inside #incomplete-tasks (by the way you set up your variables), you could use that element to delegate the event handling to. This can be done with addEventListener. You can read more about that here: https://davidwalsh.name/event-delegate.
The main reason I bring this up is because this would solve the problem of missing event handlers for your new elements. What event delegation basically means is, instead of saying:
listen at each incompleteTextbox for a focus event (which means that, when you add new elements you have to say that again for each new element)
you now say:
listen at incompleteTasksHolder for a focus event, and then determine whether that event was fired by a incompleteTextbox. That is possible because your checkboxes live INSIDE #incomplete-tasks. That could look something like this:
incompleteTasksHolder.addEventListener("focus", function(eventObject) {
if (eventObject.target && eventObject.target.matches("input[type=checkbox]")) {
// eventObject.target is the checkbox you want to work with
var checkbox = eventObject.target;
// YOUR CODE:
var item = checkbox.parentNode;
checkbox.setAttribute("checked", "checked");
completedTasksHolder.appendChild(item);
}
});
That means only one listener, that will always work, even for incompleteTextboxes that are inserted after that listener was set.

jQuery selector precedence when using pattern matching

I am creating a form that implements a bunch of similar elements. They are custom select boxes, created out of <ul>s.
Some of these elements are slightly different in the way I want the mousedown event to be handled though.
The way I have it set up currently is that, by appending _custom_select to the end of an elements class name, it will be treated as one of these special elements as far as CSS is concerned.
However, when the string selections is found inside a class name (that will coincidentally also end with _custom_select in order to apply the proper styling) I want to use a different mousedown event handler.
This is the relevant section of my event listener set up:
$('[class$="_custom_select"] li').mousedown(function(event){
var opt= event.target;
if(opt.className!='li_disabled' && event.which==1)
{
if(opt.className=='li_unselected'){
opt.className= 'li_selected';
}
else{
opt.className= 'li_unselected';
}
update_selections(opt.parentElement);
}
});
$('[class*="selections"]').mousedown(function(event){
var opt=event.target;
if(event.which==1){
if(opt.className=='li_unselected'){
opt.className= 'li_selected_2';
}
else{
opt.className= 'li_unselected';
}
}
});
This code works, but notice how, in the second binding, I had to bind the event listener to the ul that holds the li that is actually being clicked.(The ul is the element whose class name matches the pattern) In the first one however, I can bind the event listener directly to the li elements contained within the ul.
If I change the second jQuery selector to $('[class*="selections"] li') the event listener is never bound to the corresponding lis.
What is causing this behavior?
I am aware that I can just check event.target.tagName to ensure the event is bubbling up from an <li>, but that is not what the question is about.
I originally thought it had something to do with precedence and that the listeners weren't being bound because the lis that would have matched the second selector already matched against the first selector.
However, after implementing logging and looking at the DOM I have determined that when I change the second selector to: $('[class*="selections"] li') neither event listener is bound to the lis that match the second selector.
Here is a link to a JS fiddle of the 'working version'. If you add ' li' to the second selector and then try to click the <li>s in the box to the right, you will see that they no longer become green.
jsFiddle
https://jsfiddle.net/6sg6z33u/4/
Okay, thanks for posting the jsFiddle. This is an easy fix!
The elements in your second li are being added dynamically. When you bind to elements using the shortcut methods like .click() it only binds to the elements on the page when it initially bound
The fix: use the .on() method, which is the preferred method per jQuery foundation. This method allows for live binding meaning it will pick up on dynamic elements.
$('[class*="selections"]').on( 'mousedown', 'li', function(event) {
var opt = event.target;
if (event.which == 1) {
if (opt.className == 'li_unselected') {
opt.className = 'li_selected_2';
} else {
opt.className = 'li_unselected';
}
}
});

Event delegation issue with JQUERY

I am new in Jquery and I am trying to understand how event delegation work.
I am trying this:
$("#32298").on( 'click',function() { // event delegation
alert("df");
var imgs = document.getElementById("32298")[0].src;
alert(imgs);
});
When I click on the image with this Id I get the first alert but it doesn't work the second alert.
What am I doing wrong here?
Thank you.
If you want to perform event delegation then the second argument of the event handler function needs to be a selector to match the element that matches the one you want to be clicked.
$(document.body).on('click', "#32290", function(event) {
The problem with your code has nothing to do with event delegation. getElementById returns a single element (an id must be unique in the document), not a HTML Collection. (Compare with getElementsByTagName and getElementsByClassName which use the plural Elements.) It won't have a 0 property.
var imgs = document.getElementById("32298").src;
Since you're using jQuery, you can simply use the $(this) constructor, rather than document.getElementById():
$("#32298").on( 'click',function() {
alert("df");
var imgs = $(this).attr('src');
alert(imgs);
});
For what it's worth, this isn't event delegation by the way, it's just an event bound to an element.
If you must use vanilla JS to fetch the src attribute, you don't need to pass an index to the returned value of getElementById(), since this function returns a DOMObject, not an array. Update as follows:
$("#32298").on( 'click',function() {
alert("df");
var imgs = document.getElementById("32298").src;
alert(imgs);
});
It's also worth noting that IDs should be unique, so #32298 should reference a single element in the DOM. I don't know whether it's a typo, but it appears that you may have multiple elements with the same ID (since you use the variable name imgs - i.e. plural).
you can try this
$("#32298").click( function() {
alert("df");
var imgs = $(this).attr('src');
alert(imgs);
});

Dynamically adding onchange function to drop down with jQuery

I have a couple of drop down boxes with ids country1, country2, ... When the country is changed in a drop down the value of the country shoudl be displayed in an alert box.
if I add the onchange handler for one box like this it works fine:
$('#country1') .live('change', function(e){
var selectedCountry = e.target.options[e.target.selectedIndex].value;
alert(selectedCountry);
});
But I need to do this dynamically for all drop down boxes so I tried:
$(document).ready(function() {
$('[id^=country]') .each(function(key,element){
$(this).live('change', function(e){
var selectedCountry = e.target.options[e.target.selectedIndex].value;
alert(selectedCountry);
});
});
});
This doesn't work. No syntax error but just nothing happens when the seleted country is changed. I am sure that the each loop is performed a couple of times and the array contains the select boxes.
Any idea on that?
Thanks,
Paul
The reason .live() existed was to account for elements not present when you call the selector.
$('[id^=country]') .each(function(key,element){ iterates over elements that have an id that starts with country, but only those that exist when you run the selector. It won't work for elements that you create after you call .each(), so using .live() wouldn't do you much good.
Use the new style event delegation syntax with that selector and it should work:
$(document).on('change', '[id^=country]', function(e) {
// ...
});
Replace document with the closest parent that doesn't get dynamically generated.
Also, consider adding a class to those elements along with the id attribute.
Instead of incremental ids I'd use a class. Then the live method is deprecated but you may use on with delegation on the closest static parent or on document otherwise.
$('#closestStaticParent').on('change', '.country', function() {
// this applies to all current and future .country elements
});
You don't need an each loop this way; plus events are attached to all the elements in the jQuery collection, in this case all .country elements.

Categories