This question already has answers here:
Event binding on dynamically created elements?
(23 answers)
Closed 7 years ago.
At first I made a simple array just inside the .js file and wrote the function to make list items from it.
Then clicking on freshly created li elements should do something.
//This code works
dic[0] = "decir";
dic[1] = "querer";
dic[2] = "poder";
$(document).ready(
function verbsarray() {
for (var i = 0; i < dic.length; i++) {
var verbo = dic[i];
verbos += "<li class='h_li'>" + verbo + "</li>\n";
};
$('ul.h_list').html(verbos);
});
$(".h_li").click(function() {
alert("it works!");
//Dollar finds .h_li here
}
Since that was a prototype, then I developed that function to take items not from the static array, but from loaded JSON.
Function parses the needed items and again makes list items from them.
But the other function (that looks for clicked <li class="h_li">) now doesn't work...
//this doesnt work :-(
function verbos_from_json () {
$.getJSON('verbos.json', function verbsarray(vjson) {
for (var i = 0; i < vjson.data.length; i++) {
verbo = vjson.data[i].verb;
verbos += "<li class='h_li'>" + verbo + "</li>\n";
};
$('ul.h_list').html(verbos);
});
};
$(".h_li").click(function() {
alert("it works?.. no");
}
For dynamically rendered elements, you need to use delegate binding with jquery. Instead of click():
$(".h_li").click(function() {
alert("it works?.. no");
};
use on():
$(".h_list").on('click', '.h_li', function() {
alert("it works?.. no");
};
You have to integrate your listening function in your verbos_from_json
function verbos_from_json () {
$.getJSON('verbos.json', function verbsarray(vjson) {
for (var i = 0; i < vjson.data.length; i++) {
verbo = vjson.data[i].verb;
verbos += "<li class='h_li'>" + verbo + "</li>\n";
};
$('ul.h_list').html(verbos);
//****************************************************************
//}); // bad place : it was a typo ( edited after discussion )
//****************************************************************
// THIS PART HAS MOVED IN
$(".h_li").click(function() {
// better to off before (if is safe for you) to prevent multiple event
// $(".h_li").off('click').click(function() {
alert("it works?.. no");
}
}); // it was a typo ( edited after discussion )
};
Since the elements are added after ajax call. The click event is not attached to new elements that are added asynchronously . I will do this:
function verbos_from_json () {
$.getJSON('verbos.json', function verbsarray(vjson) {
for (var i = 0; i < vjson.data.length; i++) {
verbo = vjson.data[i].verb;
verbos += "<li class='h_li'>" + verbo + "</li>\n";
};
$('ul.h_list').html(verbos);
// bind here
$( ".h_li" ).bind( "click", function() {
alert( "It works" );
});
});
};
{OR}
As mentioned by War10ck delegation can be done instead of binding in the json.
Change:
$(".h_li").click(function() {
alert("it works?.. no");
};
To:
$( ".h_list" ).delegate( ".h_li", "click", function() {
alert("It works?.....no")
})
Related
I'm working on a project that returns GIFs from the GIPHY API, and whenever a search is executed, I'm capturing each search history item as its own button that a user can click on and see the results of the search as opposed to retyping the search again. I successfully added the buttons to the HTML with the proper classes, however, when I tried to write a simple on click event such as $('.history-btn').on('click', function() {
console.log('hello');
});
nothing appears. What is causing this? Here is my whole set of code for context:
$(document).ready(function () {
var searches = [];
function addSearch() {
$(".prev-searches").empty();
for (var i = 0; i < searches.length; i++) {
var history = $('<button>');
history.addClass("btn btn-primary history-btn");
history.attr("data-name", searches[i]);
history.text(searches[i]);
$(".prev-searches").append(history);
}
}
$('.history-btn').on('click', function() {
console.log('hello');
});
function returnSearch() {
var gifSearch = $(".search-input").val().trim();
var queryURL = 'https://api.giphy.com/v1/gifs/search?api_key=XXXXX-HIDDEN-XXXXX&q=' + gifSearch + '&limit=15&offset=0&rating=PG&lang=en';
$.ajax({
url: queryURL,
method: "GET"
}).then(function(response){
console.log(queryURL);
console.log(response);
console.log(response.data.length);
for (var i =0; i < response.data.length; i++) {
arrImg = response.data[i].images.fixed_width.url;
var newContent = '';
newContent = '<img src="' + arrImg + '">';
$('.gif-area').html(newContent);
console.log(newContent);
}
});
}
//When search is executed
$('.search-btn').on('click', function() {
event.preventDefault();
var search = $('.search-input').val().trim();
searches.push(search);
addSearch();
returnSearch();
});
function renderGIFs () {
for (var i = 0; i < response.data.length; i++) {
var newGifs = '';
newGifs += '<img src="' + response.data[i].bitly_gif_url + '"';
$(".gif-area").html(newGifs);
}
}
});
You need event delegation:
$('.prev-searches').on('click', '.history-btn', function() {
console.log('hello');
});
Resource
Understanding Event Delegation
Is the class 'history-btn' added dynamically from your addSearch function?
Then please use an element which is above in heirarchy of history-btn and bind the event to that element.
For example I bind the event to the div element,
<div id='historybtncontainer'>
<button class='history-btn'>Button</button>
</div>
and in script you can do
$('historybtncontainer').on('click', 'history-btn', function(){
console.log("hello");
});
document allows you to get a new tree of elements after dynamically adding the history-btn class to the <button>
$(document).on('click', '.history-btn', function() {
console.log('hello');
});
I have the next code working correctly:
var links = document.getElementsByClassName('register');
for(var index = 0; index < links.length; ++index)
{
links[index].addEventListener('click', function(){
var newMixpanelEvent = 'mixpanel.track("Click On Searched List", {"user is logged": "no"})';
trackEvent(newMixpanelEvent);
});
}
This is just listening for a click event, and then executing a function to create an event to Mixpanel.
Now I need to check the addEventListener function and attachEvent to make it work almost all browsers, so I do:
var links = document.getElementsByClassName('register');
for(var index = 0; index < links.length; ++index)
{
if( links[index].addEventListener ) {
links[index].addEventListener('click', function(){
var newMixpanelEvent = 'mixpanel.track("Click On Searched List", {"user is logged": "no"})';
trackEvent(newMixpanelEvent);
});
} else if( links[index].attachEvent ) {
links[index].attachEvent('onclick', function(){
var newMixpanelEvent = 'mixpanel.track("Click On Searched List", {"user is logged": "no"})';
trackEvent(newMixpanelEvent);
});
}
}
But this is not firing the events. Seems like if( links[index].addEventListener ) is not passing. Any idea of why?
Well, the above code works right. The problem was that I had not cleared cache. This is what I previously had and didn't work:
if( window.addEventListener ) {
...
But
if( links[index].addEventListener ) {
...
is working right.
As a final fallback you can add the event through onclick attribute. You should define sperate function with the logic for checking available ways for attaching event handlers.
function addCrossBrowserEvent(element, eventName, callback){
if(element.addEventListener){
element.addEventListener(eventName, callback);
}
else if(element.attachEvent){
element.attachEvent('on' + eventName, callback);
}
else{
element['on' + eventName] = callback;
}
}
Usage:
addCrossBrowserEvent(myElement, 'click', function() {
alert('clicked');
});
Note: Also you can try to design it as extension of HTMLElement prototype.
HTMLElement.prototype.addCrossBrowserEvent = function(eventName, callback) {
if(this.addEventListener){
this.addEventListener(eventName, callback);
}
else if(this.attachEvent){
this.attachEvent('on' + eventName, callback);
}
else{
this['on' + eventName] = callback;
}
}
// Usage
document.getElementById('elementId').addCrossBrowserEvent('click', function() {
// ...
});
Me and my loops again...
I'm trying to run a for loop across several divs, each being in the class "tooltipBox" but with different ids. In each of these divs is an input text field with class "tttFalloutOrder". What I want to do in the for loop is to attach a click-event-listener on each .tttFalloutOrder input field.
This is my code so far:
function installListener(elementId) {
$( "div#" + elementId + " > .tttFalloutOrder" ).on("click", function() {
alert("clicked on " + elementId);
});
}
function runSimulation() {
alert("running simulation...");
$( "#lContent h2" ).html("Simulation <b>in progress...</b>");
var agents = $( "div.tooltipBox" );
var rFOs = $( ".rFO" );
var i, j = 0;
for(i = 0, j = 0; i < agents.length, j < rFOs.length; i++, j++) {
var ttl = rFOs[j].value;
if((ttl == "undefined") || (ttl == "n")) {
continue;
} else {
// function-factory als gscheite closure,
// siehe http://stackoverflow.com/questions/750486/javascript-closure-inside-loops-simple-practical-example
// und http://stackoverflow.com/questions/3572480/please-explain-the-use-of-javascript-closures-in-loops?answertab=votes#tab-top
(function(i, ttl) {
var agentId = agents[i].id;
installListener(agentId);
/*
$( "div#" + agentId + " > .tttFalloutOrder" ).on("change keypress paste focus textInput input", function() {
alert(agentId + "just got changed!");
});
*/
setTimeout(function() {
$("div#" + agentId + " > div.offlineCover").fadeIn(500);
}, ttl*1000);
})(i, ttl);
}
}
$( "#lContent h2" ).html("Simulation <b>complete</b>");
}
As you can see, I am using a closure and even delegated the actual task of attaching the listener to another function, after reading in several SO-answers related to event-listeners in loops that this would help...though I honestly don't quite see how that would make any difference. Anyway, the click listeners still won't fire and frankly I don't understand what is - or rather what is not - happening here.
Thanks in advance - you people at SO have always found a way to point unknowing souls like me into the right direction, and I really appreciate that.
Update
Case closed due to my own stupidity...
First off, yes, I had an undefined member sitting in my installListener() function.
Second, the jQuery selector $( "div#" + elementId + " > .tttFalloutOrder" ) returned undefined, since the > operator selects the second element, which has the first element as a direct parent. However, since .tttFalloutOrder is an input field sitting inside a <form> tag, that is not the case...
I now scrapped the function installListener() and solved the issue with the following code:
function runSimulation() {
alert("running simulation...");
$( "#lContent h2" ).html("Simulation <b>in progress...</b>");
var agents = $( "div.tooltipBox" );
var rFOs = $( ".rFO" );
var waitUntilEvaluate = 0;
var i, j = 0;
for(i = 0, j = 0; i < agents.length, j < rFOs.length; i++, j++) {
var ttl = rFOs[j].value;
if((ttl == "undefined") || (ttl == "n")) {
continue;
} else {
// function-factory als gscheite closure,
// siehe http://stackoverflow.com/questions/750486/javascript-closure-inside-loops-simple-practical-example
// und http://stackoverflow.com/questions/3572480/please-explain-the-use-of-javascript-closures-in-loops?answertab=votes#tab-top
(function(i, ttl) {
var agentId = agents[i].id;
$( "div#" + agentId + " .tttFalloutOrder" ).on("input", function() {
alert(agentId + "just got changed!");
$( "div#" + agentId + " .wasChanged" ).prop("checked", true);
});
setTimeout(function() {
$("div#" + agentId + " > div.offlineCover").fadeIn(500);
}, ttl*1000);
waitUntilEvaluate = waitUntilEvaluate + ttl * 1000;
})(i, ttl);
}
}
console.log(waitUntilEvaluate);
setTimeout(function() {
$( "#lContent h2" ).html("Simulation <b>complete</b>");
evaluate();
}, waitUntilEvaluate);
}
You will find it easier to loop with jQuery.each() rather than for(). The .each() callback function will automatically trap the var you need, so there's no need to make another, inner closure.
The most likely thing that's preventing the click handler from working is listenerKind. If no such member exists, then an error will be thrown and the event thread will die.
Your biggest issue is knowing when to change the "in progress" message to "complete". As it stands, the message will change back immediately without waiting for any of the setTimeouts to complete, let alone all of them.
Personally, I would do something like this (see comments in code) :
function runSimulation() {
var $agents = $("div.tooltipBox"),
$rFOs = $(".rFO"),
$message = $("#lContent h2");
if($agents.filter('.running').length > 0) {
//Inhibit simulation if any part of an earlier simulation is still running.
return;
}
$message.html("Simulation <b>in progress...</b>");
$agents.each(function(i, agent) {
var ttl, $agent;
if(i >= $rFOs.length) {
return false;//break out of .each()
}
ttl = Number($rFOs.eq(i).val());//Any failure to cast as Number will result in NaN.
if(isNaN(ttl)) {
return true;//continue with .each()
}
$agent = $(agent).addClass('running');//Provide a testable state (see below and above)
$agent.children(".tttFalloutOrder").on('click.sim', function() {//Note: namespaced click event allows .off('click.sim') without affecting any other click handlers that might be attached.
alert("click on " + $agent.attr('id'));
});
setTimeout(function() {
$agent.children(".tttFalloutOrder").off('click.sim');//detach the handler attached with .on('click.sim') .
$agent.removeClass('running').children(".offlineCover").fadeIn(500);
if($agents.filter('.running').length == 0) {//if neither this nor any other agent is "running"
$message.html("Simulation <b>complete</b>");//Signify complete when all parts are complete
}
}, ttl*1000);
});
}
untested
If it the click actions still don't work, then I would suspect the $agent.children(".tttFalloutOrder") selector to be incorrect.
There are other ways of doing this type of thing, notably ways that exploit Deferreds/promises and jQuery.when(), but the above code (suitably debugged) should suffice.
I am currently coding an instant chatbox using jquery which will show the latest chat on top (refreshes when user send data via post request)
and push the oldest chat downward and remove it.
The problem is that if more than one latest chat is retrieved(for example, 2), two new div will be prepended but only one oldest div is removed instead of two...I tried timeout but it didnt work either..
Below are the code snippets I believe which got problem in it.
function showData(currentchatstyle, data, final){
var newchatstyle;
if (currentchatstyle == "chatone") {
newchatstyle = "chattwo";
}
else {
newchatstyle = "chatone";
}
$('div[class^="chat"]:first').before('<div class="' + newchatstyle + '" style="display:none;">' + data + ' </div>');
$('div[class^="chat"]:first').slideDown(500,"swing", function(){
$('div[class^="chat"]').last().fadeOut(500, function() {
$(this).remove();
});
});
return newchatstyle;
}
$('input[name="content"]').keyup(function(key) {
if (key.which==13) {
var author = $('input[name="author"]').val();
var content = $('input[name="content"]').val();
var lastnum = $('postn:first').text();
var chatstyle = $('div[class^="chat"]:first').attr("class");
$.post(
"chatajax.php",
{ "author": author, "content": content, "lastnum": lastnum },
function(data) {
var msg = data.split("|~|");
for (var i = 0; i < msg.length; i++) {
chatstyle = showData(chatstyle, msg[i], true);
}
}
);
}
});
Help will be very much appreciated.
The problem is that you do select also currently-fading-out divs with $('div[class^="chat"]').last(), as you don't remove them immediately but in the animation callback. You for example might immediately remove the chat class so it won't be selected in the next call to showData.
Also, you should only use one class "chat" for a similar divs and for a zebra-style give them independent classes.
var chatstyle = "one";
function showData(data, final){
chatstyle = chatstyle=="one" ? "two" : "one";
var newDiv = $('<div class="chat '+chatstyle+'" style="display:none;">'+data+'</div>');
$('div.chat:first').before(newDiv);
newDiv.slideDown(500, "swing", function(){
$('div.chat:last').removeClass('chat').fadeOut(500, function() {
// ^^^^^^^^^^^^^^^^^^^^
$(this).remove();
});
});
}
function post(data) {
return $.post(
"chatajax.php",
data,
function(data) {
var msg = data.split("|~|");
for (var i = 0; i < msg.length; i++)
showData(msg[i], true); // what's "final"?
}
);
}
$('input[name="content"]').keyup(function(key) {
if (key.which==13)
post({
"author": $('input[name="author"]').val(),
"content": $('input[name="content"]').val(),
"lastnum": $('postn:first').text() // I'm sure this should not be extracted from the DOM
});
});
I have built a dropdown menu system, everything works when tested independently, the problem I have is in the code below. I use the jQuery ready function to build the menu bar from an external array (menubar[]). Here I am trying to get the mouseover event to call the dropdown() function, but using a different argument for each anchor tag.
So rolling over the first should call dropdown(0), the second dropdown(1) and so on.
$(document).ready(function () {
for (i in menubar) {
var declaration = '<a href="' + baseurl + '/' + menubar[i].url +
'" class="menutitle">' + menubar[i].name + '</a>';
var a = $(declaration).mouseover(function () {
dropdown(i);
}).mouseout(function () {
activeTimer = setTimeout("removedropdowns()", 100);
});
$("#menu").append(a);
}
});
The code is calling dropdown(6); on each rollover. How can I pass the loop variable (i) into the mouseover function as a literal/static value!
I got this working fine in FF by using
.attr('onMouseOver','javascript:dropdown('+i+');')
but that wasn't firing for some versions of IE, so I switched to the jQuery mouseover, which fires, but I have the issue above :(
Your actual problem is that each of your mouseover callbacks uses the same i you increase i all the way up to 6, the callbacks still point to the same i and therefore all use 6 as the value.
You need to make a copy of the value of i, you can do this by using an anonymous function.
$(document).ready(function () {
// you should use (for(var i = 0, l = menubar.length; i < l; i++) here in case menubar is an array
for (var i in menubar) {
var declaration = '<a href="' + baseurl + '/' + menubar[i].url +
'" class="menutitle">' + menubar[i].name + '</a>';
(function(e) { // e is a new local variable for each callback
var a = $(declaration).mouseover(function () {
dropdown(e);
}).mouseout(function () {
activeTimer = setTimeout(removedropdowns, 100); // don't use strings for setTimeout, since that calls eval
});
$("#menu").append(a);
})(i); // pass in the value of i
}
});
$(function() {
$(menubar).each(function(i){
$("#menu").append('' + menubar[i].name + '');
});
$("#menu a").hover(
function(){
dropdown($(this).index());
},
function(){
activeTimer = setTimeout("removedropdowns()", 100);
}
);
});
First, don't use for..in but rather ordinary loop.
Second, I would just append the links first then apply the events later:
$(document).ready(function() {
for (var i = 0; i < menubar.length; i++) {
$("#menu").append('' + menubar[i].name + '');
}
$("#menu a").each(function(index) {
$(this).mouseover(function() { dropdown(index); }).mouseout(function() { activeTimer = setTimeout("removedropdowns()", 100); });
});
});
Have a look here and here.
To capture the current value of i, you need to pass it as a parameter to another function where it can be captured as a local variable:
Try using jQuery's each() function:
jQuery(function() {
jQuery.each(menubar, function(index, element) {
var declaration = '' + element.name + '';
var a = $(declaration).mouseover(function() { dropdown(index); }).mouseout(function() { activeTimer = setTimeout("removedropdowns()", 100); });
$("#menu").append(a);
});
});
In JavaScript, if you don't declare your variable, it is defined globally. To fix this, add "var" in front of your i looping variable like this. UPDATE: As Sime noticed (see comment), you also need to pass the variable into the function, otherwise you form a closure on the i.
$(document).ready(function() {
for(var i in menubar) {
var declaration = '' + menubar[i].name + '';
var a = $(declaration).mouseover(function(i) { dropdown(i); }).mouseout(function() { activeTimer = setTimeout("removedropdowns()", 100); });
$("#menu").append(a);
}
});