I have two javascript functions, one which attaches an eventhandler to an entire class of elements and the other which is called when the event handler is activated:
function attachDefinition(obj) {
var classArr = document.getElementsByClassName("flashcards");
for (let i = 0, len = classArr.length; i < len; i++) {
classArr[i].addEventListener('click', cardClicked);
}
}
function cardClicked(obj) {
console.log(this.id);
console.log(obj);
var img = document.createElement('img');
img.src = 'https://www.wordnik.com/img/wordnik_badge_a2.png';
document.getElementById(this.id).innerHTML = '';
document.getElementById(this.id).appendChild(img);
}
The above function runs without error on click. this.id logged to the console displays the id of the div element being clicked and obj logs the global object.
This is fine however I need to pass in an object created in a different function in the program. The above code only needs the obj argument added to addEventListener call but when I do that everything falls apart. This code:
function attachDefinition(obj) {
var classArr = document.getElementsByClassName("flashcards");
for (let i = 0, len = classArr.length; i < len; i++) {
classArr[i].addEventListener('click', cardClicked(obj)); //only thing I've changed!
}
}
function cardClicked(obj) {
console.log(this.id);
console.log(obj);
var img = document.createElement('img');
img.src = 'https://www.wordnik.com/img/wordnik_badge_a2.png';
document.getElementById(this.id).innerHTML = '';
document.getElementById(this.id).appendChild(img);
}
Now successfully console logs the passed in object but the line logging this.id is now undefined and I get "Unable to set property 'innerHTML' of undefined or null reference" on the innerHTML line.
I'm struggling to understand why passing in an argument would change this and how I can go about fixing it.
If we assume that your
classArr[i].addEventListener('click', cardClicked(obj);
is really
classArr[i].addEventListener('click', cardClicked(obj));
// Note ---------------------------------------------^
that's calling cardClicked and passing its return value into addEventListener, exactly the way foo(bar()) calls bar and passes its return value into foo.
But addEventListener expects a function in the second argument, and cardClicked doesn't return a function.
Instead, since you're relying on this referring to the clicked element inside cardClicked, you either need:
classArr[i].addEventListener('click', function() {
cardClicked.call(this, obj);
});
or
classArr[i].addEventListener('click', cardClicked.bind(classArr[i], obj));
The first works by responding to a click by calling cardClicked such that the this it sees is the same as the this the anonymous function receives.
The second works by using Function#bind to create a new function that, when called, will call cardClicked with this set to the avlue of classArr[i] and its first argument set to obj.
Change your classArr[i].addEventListener('click', cardClicked(obj); to this instead:
classArr[i].addEventListener('click', function() {
cardClicked(obj);
});
First off, you're missing a ) in the original. Additionally, you need to create an anonymous function when passing parameters in setInterval, otherwise the function in question will execute immediately upon reading.
when you assign a function to an event as action on trigger then the function's this would point to element which event fire on. your first implementation uses this advantage and then you can use this in your function to refer to related element.
But the problem in second implementation is that the function is called instead of assignment.
If you want to send additional data to you callback you can use function call or apply:
element.addEventListener('click', function(){
var obj = { addtional: 'data' };
myCallback.call(this, obj);
})
function myCallback (obj){
console.log(this.id, obj);
}
function.prototype.call
The call() method calls a function with a given this value and arguments provided individually.
function.prototype.apply
The apply() method calls a function with a given this value and arguments provided as an array (or an array-like object).
Related
I have elements that when I click on them I need to run a function and that function needs to know what element was clicked on.
Example A:
var elements = $(".config-cell");
for (var i = 0; i < elements.length; i++) {
elements[i].addEventListener("click", function() {
console.log("clicked");
});
}
When calling the function right there it works fine, however I don't know how to pass through the element to the function so it can use it.
So I tried using this method and found something strange.
Example B:
var elements = $(".config-cell");
for (var i = 0; i < elements.length; i++) {
elements[i].addEventListener("click", this.cellCrop());
}
When simply calling the function located elsewhere I noticed when loading the window it just automatically fires the function and then it doesn't even add the event listener so any time you click after that nothing happens.
Bottom line I would like to be able to pass through the current element being clicked on and have it fire a function. But I would like to know out of curiosity why method B works the way it does.
Learned that it knows which to use because 'forEach' has a callback with parameters
.forEach(function callback(currentValue[, index[, array]])
For instance: How does this call back know what is supposed to be
'eachName' and 'index'
var friends = ["Mike", "Stacy", "Andy", "Rick"];
friends.forEach(function (eachName, index){
console.log(index + 1 + ". " + eachName); // 1. Mike, 2. Stacy, 3. Andy, 4. Rick
});
And can you do this with .addEventListener instead of setting it as a
var?
That being said is there a way to have it pass variables with your own function? Like:
var passthrough = 5;
$(".config-cell").on("click", function(passthrough) {
var five = passthrough;
console.log(five);
});
First, this.cellCrop() calls the function, this.cellCrop passes it. So if you wanted to set the listener it would have been
elements[i].addEventListener("click", this.cellCrop);
Now to actually get the element clicked on inside the function you can do it a couple ways.
Using currentTarget / target from the event object that is automatically passed to event listeners
elements[i].addEventListener("click", function(event){
//the actual element clicked on
var target = event.target;
//the element the event was set on, in this case whatever elements[i] was
var currentTarget = event.currentTarget;
});
//same using jQuery
$(elements[i]).click(function(event){
var target = event.target;
var currentTarget = event.currentTarget;
});
Using the this keyword
elements[i].addEventListener("click", function(event){
//this will refer to whatever elements[i] was
var target = this;
});
//same using jQuery
$(elements[i]).click(function(event){
var target = $(this);
});
This would apply the same with using object method:
obj = {
cellCrop:function(event){
var target = event.target;
/* etc */
},
someOtherMethod:function(){
//...
elements[i].addEventListener("click",this.cellCrop);
//or jQuery
$(elements[i]).click(this.cellCrop);
//...
}
};
obj.someOtherMethod();
How does this call back know what is supposed to be 'eachName' and 'index'
Because documentation for the forEach method tells the person using it how it is going to be called. So you write the callback based on that documentation.
For instance the callback for forEach usually takes the form of
function callback(currentValue[, index[, array]])
Which means inside forEach() it is going to call your callback in this fashion
function forEach(callback){
//`this` inside forEach is the array
for(let i=0; i<this.length; i++){
callback(this[i], i, this);
}
}
As for passing arbitrary data to the function, there are a few ways it can be done:
Wrap a call to a function in an anonymous function and explicitly call the function with more arguments
obj = {
cellProp:function(event,a,b,c){
var element = event.currentTarget;
}
}
//normal anonymous function
elements[i].addEventListener('click',function(e){
obj.cellProp(e,1,2,3);
});
//fat arrow function
elements[i].addEventListener('click',e=>obj.cellProp(e,1,2,3))
In the above a, b and c will respectively contain the values 1,2 and 3
You can also use methods like bind which will change the thisArg(see this question to see more on that) of the function but also pass in arguments to the function
obj = {
//note event comes last as bind, call, and apply PREPEND arguments to the call
cellProp:function(a,b,c,event){
//`this` will change depending on the first
//argument passed to bind
var whatIsThis = this;
var element = event.target;
}
}
//passing obj as the first argument makes `this` refer to
//obj within the function, all other arguments are PREPENDED
//so a,b, and c again will contain 1,2 and 3 respectively.
elements[i].addEventListener('click', obj.cellProp.bind(obj,1,2,3) );
In the case of jQuery, you can also pass data in using an object at the time of setting up the event:
obj = {
cellProp:function(event){
var data = event.data;
console.log(data.five);
}
}
jQuery(elements[i]).click({five:5},this.cellProp);
Try this : You can make use of $(this)
$(".config-cell").on("click", function(){
var clickedCell = $(this);// this refers to current clicked cell which can be used to get other details or write logic around it
});
This may be a common error, but I can't find another way of doing this. I'm creating a timeline showing multiple projects using timeline.js with the following code
function createTimeline(){
for(var length = data.section.length,i = 0; i < length; i++){
var divWrapper = document.createElement("div");
if(i != data.section.length - 1)
divWrapper.setAttribute('class','timelineWrapper');
$('body').append(divWrapper);
layer[i] = new links.Timeline(divWrapper);
links.events.addListener(layer[i], 'rangechange',
function(){
var that = this;
var range = layer[i].getVisibleChartRange();
for (var j = 0; j < layer.length; j++){
if(j != i){
layer[j].setVisibleChartRange(range.start, range.end);
}
}
}
);
layer[i].draw(data.section[i].project, options);
}
}
It gives the error Cannot call method 'getVisibleChartRange' of undefined .
What is the problem here? Why is layer[i] undefined? It is not being detected during the event rangechange itself.
You must bind i within a closure to save its value for your addListener call, as i is undefined when the function in your addListener later gets called. Try replacing the third argument of your addListener call with the below:
(function(i) {
return function() {
var that = this;
var range = layer[i].getVisibleChartRange();
// Rest of code
};
}(i)); // Anonymous function binds i in a closure
The issue is caused because the unnamed function used as event handler uses its parent scope's i variable.
At the end of your loop, i==data.section.length in this scope.
This is also the i value for all your events handlers. Because of this, layer[i] is undefined, and this is causing the error message.
The easiest way to address this issue is to create a functionBuilder function taking i as parameter and returning a new function (your handler).
In this returned handler's direct parent's scope, i value will be the parameter you passed to the functionBuilder function.
I'll post some code example later today, as soon as I have access to a pc (no way I can type that from a tablet :o) )
EDIT: I've been too slow... mc10 posted more or less what I wanted to post :o)
In case you don't understand why this works, or what closures, scopes or bind means, here is an old but complete explanation:
http://blog.niftysnippets.org/2008/02/closures-are-not-complicated.html
EDIT
Let me get more to the point. I'm trying to create a psuedo promise implementation. The idea here being that I have a callback that won't be executed until an asynchronous call is received. So I'm simply queueing up all the calls to this function until the time at which it's notified that it can be executed. The queue is emptied and any further call to the function is SUPPOSED to execute immediately, but for some reason, the function is still queueing. This is because, for whatever reason, my redefinition of the runner function is not working correctly. The code below was my sleep deprived, frustrated version of every thought that went through my head. Here's the actual code:
function Promise(callback){
var queue = []
, callback = callback
, runner = function(){
queue.push({
context: this,
args: Array.prototype.slice.call(arguments, 0)
});
}
;//var
runner.exec = function(){
for(var i = 0, ilen = queue.length; i < ilen; i++){
var q = queue[i];
callback.apply(q.context, q.args);
}
runner = callback;
};
return runner;
}
test = Promise(function(){
$('<div/>').appendTo('#output').html(Array.prototype.slice.call(arguments,0).toString());
});
test(1,2);
test(3,4);
test.exec();
test(5,6);
http://jsfiddle.net/a7gaR/
I'm banging my head against the wall with this one. I'm trying to reassign variables in a function from a call outside the function itself (ideally by passing a reassignment function as a callback). In the example I posted on jsfiddle, I made a global function that, in theory, has a reference to the variables contained within its parent function. Upon calling that external function, I expect it to reassign the values that the other function is using. It doesn't seem to work this way.
window.test = function temp() {
var val = 7,
func = function() {
return val;
};
window.change = function() {
window.test.val = 555555;
$('<div>Changing ' + val + ' to ' + window.test.val +
'</div>').appendTo($output);
val = window.test.val;
temp.val = window.test.val;
func = function() {
return 'Why isn\'t this working?';
}
}
return func();
}
var $output = $('#output');
$('<div/>').appendTo($output).html('::' + test() + '::');
window.change();
$('<div/>').appendTo($output).html('::' + test() + '::');
http://jsfiddle.net/YhyMK/
The second time you call test you're creating a new local variable called func and defining a new window.change that closes over that new variable. The changes you made to the original func by calling the original window.change are not relevant in the second call.
Also note that the following line:
window.test.val = 555555;
...does not modify/refer to the val variable in the outer function. window.test.val refers to a property named val on the test object (which happens to be a function), not any local variable.
You are trying to refer to a local variable in a function with the syntax func.varname. That won't work, that's not the way local variables work.
I finally created a function that would perform this operation. The gist for it is here: https://gist.github.com/2586972.
It works like this...
You make a call to Defer passing the callback whose functionality you'd like to delay:
var deferredCB = Defer(function(){ console.log(this,arguments) };
deferredCB will now store all of the arguments you pass allowing them to be executed at some later date:
defferedCB(1);
defferedCB(2);
Now, when you're ready to perform the operation, you simply "execute" deferredCB:
defferedCB.exec();
Which results in:
// window, 1
// window, 2
All future calls to deferredCB will be executed immediately. And now that I'm thinking about it, I'll probably do a rewrite to allow you to reset deferredCB to it's pre-executed state (storing all the arguments again).
The key to making it work was having a wrapper function. Javascript simply won't let you reassign a function while it's being executed.
TADA!!!
function myFunction(par) {
...
}
el1.onclick=myFunction(5);
el2.onclick=myFunction(7);
How to get a reference for the caller element inside myFunction?
In your example, you are assigning the return value from myFunction(5) to el1.onclick, and the return value from myFunction(7) to el2.onclick.
Take the following example:
el1.onclick=myFunction(5);
In the above example, myFunction(5) is executed. Then the return value of that function, let's call it myval, is assigned to el1.onclick. This is equivalent to:
var myval = myFunction(5);
el1.onclick = myval;
It's not clear that this is what you intended. For this to work, the return value from myFunction would need to be a function (or a string of Javascript) to be executed later.
It is inside that function that you would refer to the caller element. this inside that function will return the element on which the event is currently being called.
//this is saying call this function and asign what it returns to onclick
el1.onclick= myFunction(5);
You want to use a closure
el1.onclick = function(){myFunction(5);}
To reference the element you can pass the object in
function test(elem, x){
elem.innerHTML = x;
}
document.getElementById("foo").onclick = function(){ test(this, 1); }
example 1
or you can use call so that within the listener, this references the element.
function test(x){
this.innerHTML = x;
}
document.getElementById("foo").onclick = function(){ test.call(this, 1); }
example 2
function createTextFields(obj) {
for (var i = 0; i < obj.length; i++) {
dataDump = {};
for (var key in obj[i]) {
dataDump[key] = textField.value;
var callback = function (vbKey) {
return function (e) {
dataDump[vbKey] = e.source.value;
};
}(key);
}
}
globalData.push(dataDump);
}
I create textFields on click of button, each time a new one is created from the object. When i change the First or Second or Third TextFields and click on update... the value get's updated on the fourth or the last TextFields TextField's Object...
The callback is called on change of the TextFields
It's hard to tell without context, but assuming that different instances of callback are called on change of set text fields, then the problem with closure context of callback.
Callback function holds reference on global dataDump object which is re-assigned on each cycle of the iteration. So, at the end of the for loop all callbacks would reference only one (latest) dataDump object.
Try to add "var" to assignment line.
var dataDump = {};
You are using window.dataDump in this bit of code, so all your callback functions and such are using this same global variable.
Try var dataDump = {}; instead. You'll probably also want to move your globalData.push(dataDump); inside the loop.