When you specify a function (with or without parameters) in the DOM via onChange, you are limited to static paramters. E.g.
<select id="blah" onChange="doSomething(3);">
However, jQuery lets you dynamically define and assign functions to event handlers, such as 'change'.
See the example below:
function addRow(customObject) {
<< Create TR containing a select named "#someSelect + customObject.id" >>
// Demo
$("#someSelect" + customObject.id).change(function() {
changeSomething(customObject)
});
}
My question is - essentially - will this work? I am using a specific instance o
f an object in my jQuery change event handler. Will that particular instance of customObject (wherever it lies) always be bound to this event handler callback?
customObject is initialized at load, and is one of many. For that particular select box, I always want it to be linked to that object.
New to jQuery. Thanks in advance.
EDIT:
I have several rows of items. Each is represented by an object. I just want to make sure that when someone modifies one of the rows via HTML, that the underlying object (bound to the callback function) is properly updated.
I am looking for something like this, conceptually a mapping between:
Row 1 - customObject1
Row 2 - customObject2
Row 3 - customObject3
... such that if I modify one of the HTML elements (e.g. change the select box) in row 3, that behind the scenes, customObject3 is modified
What you have will work in case customObject.id is defined before adding the listener and you intent the listener itself to be bound to that specific element without moving it around.
From your question I understand that customObject.id is dynamic as in can change its value and depending on this value, the listener can listen on different element. In this case, you'd have to use a bit more general selector and check the value inside the listener itself:
$('select').on('change', function(e) {
if(this.id === 'someSelect' + customObject.id)
return myCallback(e);
});
Edit:
The problem lies in accessing the customObject inside the listener. It uses the current content, not the content it used to have when you added the listener.
Easiest solustion I know is:
var select = $("#someSelect" + customObject.id);
select.customObject = customObject; /* save customObject to the select itself */
select.on('change', function() {
changeSomething(this.customObject); /* don't use global object but its own */
});
Related
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.
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';
}
}
});
Thank you in advance for looking at this.
My webapp allows a user to select choices from four different drop-down menus. When the user makes a selection, the program successfully performs the following click() function which creates a new span element within a div element:
var activeFilterNames = [];
$('.filter').click(function()
{
if (!this.classList.contains('disabled'))
{
//above checks to see if link has already been clicked
//and is therefore disabled. If not, go ahead.
activeFilterNames.push(this.textContent);
//above adds name of selected link to array
i = activeFilterNames.length;
var newFilter = document.createElement('span');
newFilter.id = activeFilterNames[i-1];
newFilter.className = this.className+" activated";
//above adds a new class to identify the filter as 'activated'
//above retains existing classname to identify
//which of the four filters it came from
newFilter.innerHTML = activeFilterNames[i-1];
//above creates display text to be rendered in browser
document.getElementById('active_filters').appendChild(newFilter);
//above is the div in which span will be appended to other spans.
$("#active_filters > span").removeClass('filter');
//above removes .filter class so that this newly created span does
//not respond to the .filter click() function.
$(this).addClass('disabled');
//above adds 'disabled' class to the originally
//clicked link, causing it to skip this block of code
}
}
);
Everything appears to work fine up to this point (though I may be missing something). Essentially I am creating span elements that come out looking like this in html:
<span id="id_name" class="menu_name activated">Rendered Name</span>
And since the above span does not have the filter class, I then try to create a new function in javascript (just as a test) to make the element responsive:
$('.activated').click(function()
{
alert('hi');
}
);
But no luck. I've tried to re-render the dynamically created elements by nesting div or a inside the span, modifying the code as needed, but still nothing. I would like to keep the span because it's the only way I've found to wrap these dynamically generated elements to a second line within the div (#active_filters) where they are being created.
Can anyone see what I'm doing wrong given that I want to make the activated click() function responsive within each newly created span element?
Your binding will not work if you attempt to bind to DOM elements contained in $('.activated') before creating them. What this usually means is that you need that event listener to bind after creating. If you're dynamically creating DOM elements, you need to do something like this:
var activeFilterNames = [];
$('.filter').click(function()
{
if (!this.classList.contains('disabled'))
{
activeFilterNames.push(this.textContent);
i = activeFilterNames.length;
var newFilter = document.createElement('span');
newFilter.id = activeFilterNames[i-1];
newFilter.className = this.className+" activated";
newFilter.innerHTML = activeFilterNames[i-1];
document.getElementById('active_filters').appendChild(newFilter);
$('.activated').unbind('click');
$('.activated').click(function()
{
alert('hi');
}
);
$("#active_filters > span").removeClass('filter');
$(this).addClass('disabled');
}
}
);
Notice, before binding we unbind. This makes sure that if you do this multiple times, you aren't binding 2, 3, 4 times to the same DOM element.
You need to attach click event on dynamically created elements. In jQuery this can be done using on method if you will pass your selector as second argument and attach click to some parent element, body for example:
$( 'body' ).on( 'click', '.activated', function()
{
alert('hi');
}
);
I'm making a group of elements from a JSON data:
Example:
{
'name':'form',
'elements':[
{'name':'bt1','type':'button','value':'hello','order':'1'},
{'name':'img1','type':'image','value':'http://www.images.com/img.jpg','order':'2'}]
}
What i do with this json is create a form with the elements described in 'elements' with a code like this:
(I've got this draft in mumbo jumbo + jquery code)
$('#container').html();//clears the container
for each element in elements do
switch element.type
case 'button':
$('#container').append('<input type="submit" value="'+element.value + ... etc');
end case
case 'image':
insert image bla bla bla
end switch
end for each
I want to detect if an element gets clicked or another kind of action, like mouse hover, etc. How do i bind this to the elements?
Also, how do i update the elements without destroying them?
EDIT: I implied something important, my bad:
I need to link the data in the elements javascript object with the generated html elements. A data field wich i retrieve when an action is triggered. That's the porpouse of all this.
You have two options. You can bind the listeners after you've created the elements, like this:
var $input = $('<input type="submit" value="'+element.value + ... etc')
.focus(...).blur(...).etc.;
$('#container').append($input);
Or, you can use event delegation. On your initial page load you can do this:
$("#container").on( "focus", "input", function(){...});
This will cover all input elements in #container either currently or dynamically added later. You can read more about event delegation in the on docs.
Building the form is really very easy, since you've basically mapped all of the attributes of the elements in an object sytanx. As such, we can create these elements with nothing more than choosing a tag, and passing the attribute object in as the second parameter of the jQuery function:
/* Container reference, counting variable */
var container = $("#container"), i = 0;
/* Clear out the container */
container.html("");
/* Cycle through each element */
while ( current = data.elements[i++] ) {
/* Evaluate the value of the current type */
switch ( current.type ) {
/* Since <input type='button|image'> are so similar, we fall-through */
case "button":
case "image" :
/* Choose a base element, pass in object of properties, and append */
$("<input>", current).appendTo(container);
break;
}
}
When it comes to registering clicks, or any other type of event, we'll use the $.on method. Because we're passing in a selector ( "input" in this case ), this will not only match all present elements, but all future elements as well.
/* Listen for all clicks on input elements within the container */
container.on("click", "input", function(){
/* We have a button, and an image. Alert either the value or src */
alert( this.value || this.src );
});
Online Demo: http://jsbin.com/izimut/edit#javascript,html
To detect events on dynamically added elements, you should use on() for jQuery 1.7+ and .live() for previous versions.
EDIT: And yes, as James pointed out in the comments, delegate() is always recommended over live().
if your js code is short, just add your js code in the append function.
append('<input type="submit" onclick="xxxx" value="'+element.value + ... etc');
if your js code is long, you can add an id to your new element.
and add event to the id.
$("#idxx").click(function(){alert("Hello");});
Either bind the element directly
$input = $('<input type="submit" value="'+element.value + ... etc');
$input.on('click', function() {
// do something
}
$('#container').append($input);
or put the bind on a parent that checks the select of what was click inside..
$('#container').on('click', 'input', function() {
// do something
}
What I am trying to do is clone 3 drop-down boxes and add them beneath the original set.
At the moment it works but the clones do not maintain the functionaility of the original set.
What the originals do is check the selected value of the first drop-down box in order to populate the drop-down lists for the other two.
Fiddle is below but adding the clones doesn't seem to work for some reason I can't figure out, it works on the page I am working on.
http://jsfiddle.net/pV6x5/6/
Thanks,
Martin
UPDATE
updated the fiddle, it was missing the advancedsearch div: http://jsfiddle.net/pV6x5/7/
Use jQuery live to keep the event's bound to the new elements. With live it binds the event to all present and future elements where just defining a handler for change will only bind for the current elements (you can also just reattach the events every time you create an element but why do it when live takes care of it for you)
$tags.live("change",function(){ /* your stuff here */});
UPDATE Here is the change function and if block:
$(document).ready(function()
{
$tags = $("select[name='tags']");
$tags.live("change",function()
{
$operands = $(this).parent().find("select[name='operands']");
$values = $(this).parent().find("select[name='values']");
if ($(this).val() == "agent")
{
$(this).parent().find("select[name='operands'] option").remove();
$("<option>=</option>").appendTo($operands);
$("<option>!=</option>").appendTo($operands);
$(this).parent().find("select[name='values'] option").remove();
$("<option>excel</option>").appendTo($values);
$("<option>msword</option>").appendTo($values);
$("<option>ppt</option>").appendTo($values);
$("<option>pdf</option>").appendTo($values);
$("<option>image</option>").appendTo($values);
$("<option>txt</option>").appendTo($values);
$("<option>html</option>").appendTo($values);
$("<option>csv</option>").appendTo($values);
$("<option>ooxml</option>").appendTo($values);
$("<option>flash</option>").appendTo($values);
$("<option>wmf</option>").appendTo($values);
}
I believe you need to use the .live() bind, so that it attaches the events to the objects made "in the future."
http://api.jquery.com/live/
So rather than .change() you need .live('change')