In my web page I want to be able to export multiple tables as csv. I can code these individually (like this), but my for loop isn't working.
// this works
$("#xx001").on('click', function (event) { exportTableToCSV.apply(this, [$('#table001'), 'export.csv']); });
$("#xx002").on('click', function (event) { exportTableToCSV.apply(this, [$('#table002'), 'export.csv']); });
// this fails
let myxx = "";
let mytable = "";
for (let i = 0; i < 5; i++) {
myxx += "xx00" + i ;
table += "table00" + i ;
$('#'+${myxx}).on('click', function (event) { exportTableToCSV.apply(this, [$( '#'+${table} ), 'export.csv']); });
});
I expected that the both tables could be exported, but I'm getting a "File not found" error using the loop.
Your function inside of the loop will call that last myxx and table which both are 005 at the time of firing event ( when the loop is iterated until the end, and those two variables got filled with the last value of loop ).
If you need more explanation about what dafuq is happening, you should check these articles about closures. article one and article two. There are tons of more resources out there, but I just found these two at the moment.
You need to pass the right variable to it just like this:
for (let i = 0; i < 5; i++) {
let myxx = "xx00" + i ;
let table = "table00" + i ;
(function( x, t ) {
$('#' + x ).on('click', function (event) { exportTableToCSV.apply(this, [$( '#' + t ), 'export.csv']); });
})( myxx, table );
};
the above code should do the job for you.
It shouldn't be necessary to create a loop for this. A single jQuery statement can bind a listener to every button.
$('.btn').on('click', function (event)...
The trick is that apply() needs the id of the relevant table. That can be provided dynamically by traversing the DOM relative to the particular button that was pressed.
If the table element is followed by the button (as it is in the linked example), then we only need to reference the previous element:
$('.btn').on('click', function (event) { exportTableToCSV.apply(this, [$(this).prev().attr("id"), 'export.csv'])
});
Note that prev() can accept selector arguments, so even if the table isn't the immediately preceding sibling element you could use a selector to find it something like prev("table")
Related
I have many these links in my template
Add
And need click to the link and get certain data-slug. I try use JS and get all links so.
var add_to_cart = document.getElementsByClassName('add_to_cart');
After I try for
for(var i = 0; i < add_to_cart.length; i++) {
product_slug = add_to_cart[i].getAttribute('data-slug')
add_to_cart[i].onclick = function() {
console.log(product_slug)
}
}
And after click always console.log have last data-slug latest link in my template. How to fix it and get the data-slug of the item I clicked on. Help me, please.
Your for loop does not work because the value of product_slug is set when the loop runs.
Because product_slug is a var you overwrite it each time the loop runs. And when the click listener is called product_slug will have the same value it had when the loop ran for the last time.
var creates a variable with function scope, it is defined in the scope of the function.
let or const creates a variable with block scope, it is defined in the scope of the block (inside the curly braces { }).
Read more about scope.
So you want to use let.
for(var i = 0; i < add_to_cart.length; i++) {
let product_slug = add_to_cart[i].getAttribute('data-slug')
add_to_cart[i].onclick = function() {
console.log(product_slug)
}
}
You are defining a variable that is written n times during a for loop, and n events that will display the value. That's why you have the same output each time.
What you can do is get the current target of your onclick event, and then display it's data-slug attribute
your for loop would look like this :
var add_to_cart = document.getElementsByClassName('add_to_cart');
for(var i = 0; i < add_to_cart.length; i++) {
add_to_cart[i].onclick = function(evt) {
console.log(evt.target.getAttribute('data-slug'));
}
}
You want to use the target of the click. That's the .target attribute of the first argument to the event handler.
For example, the following code assigns an event handler to all the add_to_cart elements on the page and logs the slug to the console when they're clicked:
let add_to_cart_buttons = document.querySelectorAll('.add_to_cart');
add_to_cart_buttons.forEach(function(node) {
node.addEventListener('click', (e) => console.log(e.target.dataset.slug));
});
EDIT: Apologies, I misread your second code block when I first wrote this. Your use of document.getElementsByClassName is correct.
I'm a javascript/dom noob coming from mainly Python and Clojure, and the semantics of referring to things in the dom are really confusing me.
Take the following extract from some code which I just wrote into a chrome extension to identify a subset of pages matching criteria defined in testdownloadpage and then hang eventlisteners on pdf download links to intercept and scrape them.:
function hanglisteners() {
if (testdownloadpage(url)) {
var links = document.getElementsByTagName("a");
for (i = 0, len = links.length; i < len; i++) {
var l = links[i];
if (testpdf(l.href)) {
l.addEventListener("click", function(e) {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
getlink(this.href);
}, false);
};
};
};
};
Originally, I had the call to getlink at the bottom reading getlink(l.href), and I thought (evidently naively) that what would happen with that code would be that it would loop through every matching link, and slap a listener on each that called getlink on the url for that link. But that didn't work at all. I tried replacing l.href with this.href just as a guess, and it started working.
I'm not sure why this.href worked when l.href did not. My best guess is that the javascript interpreter doesn't evaluate l.href in an addEventListener call until some point later, when l has changed to something else(??). But I'm not sure why that should be, or how to know when javascript does evaluate arguments to a function call and when it doesn't...
And now I'm worrying about the higher-up call to testpdf(l.href). That function is meant to check to make sure a link is a pdf download link before hanging the listener on it. But is that going to evaluate l.href within the loop, and hence correctly evaluate each link? Or is that also going to evaluate at some point after the loop, and should I be using this.href instead?
Can some kind soul please explain the underlying semantics to me, so that I don't have to guess whether referring to the loop variable or referring to this is correct? Thanks!
EDIT/ADDITION:
The consensus seems to be that my problem is a (clearly well-known) issue where inner functions in loops are victims of scope leaks s.t. when they're called, unless the inner function closes over all the variables it uses, they end up bound to the last element of the loop. But: why does this code then work?
var links = document.getElementsByTagName("a");
for (i = 0, len = links.length; i < len; i++) {
let a = links[i];
a.addEventListener("click", function(e) {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
console.log(a.href);
});
};
<html>
<head>
<title>silly test</title>
</head>
<body>
<p>
Link 1
link2
link 3
</p>
</body>
</html>
Based on those answers, I'd expect clicking on every link to log "link 3," but they actually log the correct/expected naive results...
The problem arises because the loop variable has already changed by the time the listener actually fires. You should be able to see this with the example below.
function setup(){
var root = document.getElementById('root');
var info = document.createElement('div');
for (var i = 0; i < 5; i++){
// create a button
var btn = document.createElement('button');
// set up the parameters
btn.innerHTML = i
btn.addEventListener('click', function(event){
info.innerHTML = ('i = ' + i + ', but the button name is ' + event.target.innerHTML);
})
// add the button to the dom
root.appendChild(btn)
}
var info = document.createElement('div');
info.innerHTML = 'The Value of i is ' + i
root.appendChild(info)
}
// run our setup function
setup();
<div id="root">
</div>
so what you need to do is save off a copy of l for later use. addEventListener is automatically storing the element in this for exactly this reason. Another very good way to do this, however if you don't want to mess with this for some reason is to use a generator function (a function that creates a function).
for instance:
function clickListener(i, info){
return function(event){
info.innerHTML = ('i = ' + i + ', but the button name is ' + event.target.innerHTML);
}
}
This way we can put the variables i and info into scope for the listener function which is called later without the values changing.
putting this code into our example above, we get the following snippet
function clickListener(i, info){
return function(event){
info.innerHTML = ('i = ' + i + ', but the button name is ' + event.target.innerHTML);
}
}
function setup(){
var root = document.getElementById('root');
var info = document.createElement('div');
for (var i = 0; i < 5; i++){
// create a button
var btn = document.createElement('button');
// set up the parameters
btn.innerHTML = i
// use the generator to get a click handler
btn.addEventListener('click', clickListener(i, info))
// add the button to the dom
root.appendChild(btn)
}
info.innerHTML = 'The Value of i is ' + i
root.appendChild(info)
}
// run our setup function
setup();
<div id="root">
</div>
EDIT: Answering revised question
In your second iteration of code, you use the let keyword introduced with ES6. This keyword is specifically designed to give javascript variables more traditional block scope. In our case it makes the variable scoped to the specific iteration of the loop. A similar example can be seen on MDN. If you can support ES6 in your application/build system, using let is a great way to solve the problem.
When you're in the function of event listeners, this is bound to the DOM element for which the event listener is for. Imagine the following code:
var links = document.getElementsByTagName("a");
for (i = 0, len = links.length; i < len; i++) {
let a = links[i];
a.addEventListener("click", function(e) {
console.log(this === a); // => true
})
}
The two would be identical, so you can use either whenever
I'm not sure what is wrong with my code. I keep getting A NaN variable for maxLength. Am I writing this function correctly?
Helper function I am trying to call:
(function($) {
$.fn.countRemainingCharactersHelper = function(maxLength) {
id = this.attr('id');
var textLength = this.val().length;
var textRemaining = maxLength - textLength;
if (textLength >= maxLength) {
textRemaining = 0;
}
var messageId = id.slice(0, (id.indexOf('someTxtBox'))) + 'someTxtBox';
$('#' + messageId).html(textRemaining + ' characters remaining.');
};
})(jQuery);
Functions calling to the helper function above:
function countRemainingCharacters() {
$(this).countRemainingCharactersHelper(1000);
}
function countRemainingCharacters(maxLength) {
$(this).countRemainingCharactersHelper(maxLength);
}
Calling to function passing in the maxLength variable
$('#samplesomeTxtBox').click(function() {
countRemainingCharacters(4000);
});
this will refer to the window in your countRemainingCharacters() functions as you don't call it with a scope. You can fix that using call():
$('#samplesomeTxtBox').click(function() {
countRemainingCharacters.call(this, 4000);
})
Also note that the logic you use to generate the id of the element to place the message in is a little off. You could improve it by using a data attribute to explicitly set the element id.
Your overloaded method is also redundant in this case as you can take advantage of JavaScripts 'falsy' logic to provide a default value if none was provided. This means that you can convert your two countRemainingCharacters() functions in to one:
function countRemainingCharacters(maxLength) {
$(this).countRemainingCharactersHelper(maxLength || 1000);
}
Working example
All that would be left at that point is to create a key event handler to calculate the remaining characters as the user types.
I seem to have an issue when creating copies of a template and tying the .click() method to them properly. Take the following javascript for example:
function TestMethod() {
var test = Array();
test[0] = 0;
test[1] = 1;
test[2] = 2;
// Insert link into the page
$("#test_div").html("<br>");
var list;
for (x = 0; x < test.length; x++) {
var temp = $("#test_div").clone();
temp.find('a').html("Item #" + test[x]);
temp.click(function () { alert(x); });
if (list == undefined)
list = temp;
else
list = list.append(temp.contents());
}
$("#test_div2").append(list);
}
The problem I am seeing with this is that no matter which item the user clicks on, it always runs alert(2), even when you click on the first few items.
How can I get this to work?
Edit: I have made a very simple example that should show the problem much clearer. No matter what item you click on, it always shows an alert box with the number 2 on it.
Correct me if I'm wrong, .valueOf() in JS returns the primitive value of a Boolean object.....
this would not happen ShowObject(5,'T');... ShowObject(objectVal.valueOf(), 'T');
why not use objects[x].Value directly? ShowObject(objects[x].Value, 'T');
WOOOOOSSSHHHH!
after searching deeply... I found a solution...
because it's a closure, it won't really work that way...
here's a solution,
temp.find('a').bind('click', {testVal: x},function (e) {
alert(e.data.testVal);
return false;
});
for best explanation, please read this... in the middle part of the page where it says Passing Event Data a quick demo of above code
I think your issue arises from a misunderstanding of scopes in JavaScript. (My apologies if I'm wrong.)
function () {
for (...) {
var foo = ...;
$('<div>').click(function () { alert(foo); }).appendTo(...);
}
}
In JavaScript, only functions create a new scope (commonly referred to as a closure).
So, every round of the for loop will know the same foo, since its scope is the function, not the for. This also applies to the events being defined. By the end of looping, every click will know the same foo and know it to be the last value it was assigned.
To get around this, either create an inner closure with an immediately-executing, anonymous function:
function () {
for (...) {
(function (foo) {
$('<div>').click(function () { alert(foo); }).appendTo(...);
})(...);
}
}
Or, using a callback-based function, such as jQuery.each:
function () {
$.each(..., function (i, foo) {
$('<div>').click(function () { alert(foo); }).appendTo(...);
});
}
For your issue, I'd go with the latter (note the changes of objects[x] to just object):
var list;
jQuery.each(data.objects, function (x, object) {
// Clone the object list item template
var item = $("#object_item_list_template").clone();
// Setup the click action and inner text for the link tag in the template
var objectVal = object.Value;
item.find('a').click(function () { ShowObject(objectVal.valueOf(), 'T'); }).html(object.Text);
// add the html to the list
if (list == undefined)
list = item;
else
list.append(item.contents());
});
I edited the question so it would make more sense.
I have a function that needs a couple arguments - let's call it fc(). I am passing that function as an argument through other functions (lets call them fa() and fb()). Each of the functions that fc() passes through add an argument to fc(). How do I pass fc() to each function without having to pass fc()'s arguments separately? Below is how I want it to work.
function fa(fc){
fc.myvar=something
fb(fc)
}
function fb(fc){
fc.myothervar=something
fc()
}
function fc(){
doessomething with myvar and myothervar
}
Below is how I do it now. As I add arguments, it's getting confusing because I have to add them to preceding function(s) as well. fb() and fc() get used elsewhere and I am loosing some flexibility.
function fa(fc){
myvar=something
fb(fc,myvar)
}
function fb(fc,myvar){
myothervar=something
fc(myvar,myothervar)
}
function fc(myvar,myothervar){
doessomething with myvar and myothervar
}
Thanks for your help
Edit 3 - The code
I updated my code using JimmyP's solution. I'd be interested in Jason Bunting's non-hack solution. Remember that each of these functions are also called from other functions and events.
From the HTML page
<input type="text" class="right" dynamicSelect="../selectLists/otherchargetype.aspx,null,calcSalesTax"/>
Set event handlers when section is loaded
function setDynamicSelectElements(oSet) {
/**************************************************************************************
* Sets the event handlers for inputs with dynamic selects
**************************************************************************************/
if (oSet.dynamicSelect) {
var ySelectArgs = oSet.dynamicSelect.split(',');
with (oSet) {
onkeyup = function() { findListItem(this); };
onclick = function() { selectList(ySelectArgs[0], ySelectArgs[1], ySelectArgs[2]) }
}
}
}
onclick event builds list
function selectList(sListName, sQuery, fnFollowing) {
/**************************************************************************************
* Build a dynamic select list and set each of the events for the table elements
**************************************************************************************/
if (fnFollowing) {
fnFollowing = eval(fnFollowing)//sent text function name, eval to a function
configureSelectList.clickEvent = fnFollowing
}
var oDiv = setDiv(sListName, sQuery, 'dynamicSelect', configureSelectList); //create the div in the right place
var oSelected = event.srcElement;
if (oSelected.value) findListItem(oSelected)//highlight the selected item
}
Create the list
function setDiv(sPageName, sQuery, sClassName, fnBeforeAppend) {
/**************************************************************************************
* Creates a div and places a page in it.
**************************************************************************************/
var oSelected = event.srcElement;
var sCursor = oSelected.style.cursor; //remember this for later
var coords = getElementCoords(oSelected);
var iBorder = makeNumeric(getStyle(oSelected, 'border-width'))
var oParent = oSelected.parentNode
if (!oParent.id) oParent.id = sAutoGenIdPrefix + randomNumber()//create an ID
var oDiv = document.getElementById(oParent.id + sWindowIdSuffix)//see if the div already exists
if (!oDiv) {//if not create it and set an id we can use to find it later
oDiv = document.createElement('DIV')
oDiv.id = oParent.id + sWindowIdSuffix//give the child an id so we can reference it later
oSelected.style.cursor = 'wait'//until the thing is loaded
oDiv.className = sClassName
oDiv.style.pixelLeft = coords.x + (iBorder * 2)
oDiv.style.pixelTop = (coords.y + coords.h + (iBorder * 2))
XmlHttpPage(sPageName, oDiv, sQuery)
if (fnBeforeAppend) {
fnBeforeAppend(oDiv)
}
oParent.appendChild(oDiv)
oSelected.style.cursor = ''//until the thing is loaded//once it's loaded, set the cursor back
oDiv.style.cursor = ''
}
return oDiv;
}
Position and size the list
function configureSelectList(oDiv, fnOnClick) {
/**************************************************************************************
* Build a dynamic select list and set each of the events for the table elements
* Created in one place and moved to another so that sizing based on the cell width can
* occur without being affected by stylesheet cascades
**************************************************************************************/
if(!fnOnClick) fnOnClick=configureSelectList.clickEvent
if (!oDiv) oDiv = configureSelectList.Container;
var oTable = getDecendant('TABLE', oDiv)
document.getElementsByTagName('TABLE')[0].rows[0].cells[0].appendChild(oDiv)//append to the doc so we are style free, then move it later
if (oTable) {
for (iRow = 0; iRow < oTable.rows.length; iRow++) {
var oRow = oTable.rows[iRow]
oRow.onmouseover = function() { highlightSelection(this) };
oRow.onmouseout = function() { highlightSelection(this) };
oRow.style.cursor = 'hand';
oRow.onclick = function() { closeSelectList(0); fnOnClick ? fnOnClick() : null };
oRow.cells[0].style.whiteSpace = 'nowrap'
}
} else {
//show some kind of error
}
oDiv.style.width = (oTable.offsetWidth + 20) + "px"; //no horiz scroll bars please
oTable.mouseout = function() { closeSelectList(500) };
if (oDiv.firstChild.offsetHeight < oDiv.offsetHeight) oDiv.style.height = oDiv.firstChild.offsetHeight//make sure the list is not too big for a few of items
}
Okay, so - where to start? :) Here is the partial function to begin with, you will need this (now and in the future, if you spend a lot of time hacking JavaScript):
function partial(func /*, 0..n args */) {
var args = Array.prototype.slice.call(arguments, 1);
return function() {
var allArguments = args.concat(Array.prototype.slice.call(arguments));
return func.apply(this, allArguments);
};
}
I see a lot of things about your code that make me cringe, but since I don't have time to really critique it, and you didn't ask for it, I will suggest the following if you want to rid yourself of the hack you are currently using, and a few other things:
The setDynamicSelectElements() function
In this function, you can change this line:
onclick = function() { selectList(ySelectArgs[0], ySelectArgs[1], ySelectArgs[2]) }
To this:
onclick = function() { selectList.apply(null, ySelectArgs); }
The selectList() function
In this function, you can get rid of this code where you are using eval - don't ever use eval unless you have a good reason to do so, it is very risky (go read up on it):
if (fnFollowing) {
fnFollowing = eval(fnFollowing)
configureSelectList.clickEvent = fnFollowing
}
And use this instead:
if(fnFollowing) {
fnFollowing = window[fnFollowing]; //this will find the function in the global scope
}
Then, change this line:
var oDiv = setDiv(sListName, sQuery, 'dynamicSelect', configureSelectList);
To this:
var oDiv = setDiv(sListName, sQuery, 'dynamicSelect', partial(configureSelectListAlternate, fnFollowing));
Now, in that code I provided, I have "configureSelectListAlternate" - that is a function that is the same as "configureSelectList" but has the parameters in the reverse order - if you can reverse the order of the parameters to "configureSelectList" instead, do that, otherwise here is my version:
function configureSelectListAlternate(fnOnClick, oDiv) {
configureSelectList(oDiv, fnOnClick);
}
The configureSelectList() function
In this function, you can eliminate this line:
if(!fnOnClick) fnOnClick=configureSelectList.clickEvent
That isn't needed any longer. Now, I see something I don't understand:
if (!oDiv) oDiv = configureSelectList.Container;
I didn't see you hook that Container property on in any of the other code. Unless you need this line, you should be able to get rid of it.
The setDiv() function can stay the same.
Not too exciting, but you get the idea - your code really could use some cleanup - are you avoiding the use of a library like jQuery or MochiKit for a good reason? It would make your life a lot easier...
A function's properties are not available as variables in the local scope. You must access them as properties. So, within 'fc' you could access 'myvar' in one of two ways:
// #1
arguments.callee.myvar;
// #2
fc.myvar;
Either's fine...
Try inheritance - by passing your whatever object as an argument, you gain access to whatever variables inside, like:
function Obj (iString) { // Base object
this.string = iString;
}
var myObj = new Obj ("text");
function InheritedObj (objInstance) { // Object with Obj vars
this.subObj = objInstance;
}
var myInheritedObj = new InheritedObj (myObj);
var myVar = myInheritedObj.subObj.string;
document.write (myVar);
subObj will take the form of myObj, so you can access the variables inside.
Maybe you are looking for Partial Function Application, or possibly currying?
Here is a quote from a blog post on the difference:
Where partial application takes a function and from it builds a function which takes fewer arguments, currying builds functions which take multiple arguments by composition of functions which each take a single argument.
If possible, it would help us help you if you could simplify your example and/or provide actual JS code instead of pseudocode.