Assigning of value in a for loop on a mouse event - javascript

Why do I always get the last value assigned to the variable
even though I already enclosed it in a function?
When the event mouse up is triggered and getGoogleFiles is called, the last value assigned to resourceId is called. I don't get it.
for ( var i in arrayObj) {
var resourceId = arrayObj[i].ResourceId;
entity_list.onmouseup = function(event) {
parent.changeButtonState(this, event);
(function(resourceId) {
getGoogleFiles(resourceId);
})(resourceId);
}
}
Note: This is different to other JavaScript questions because the onmouseup is not triggered
I followed the creating of another function mentioned here:
JavaScript closure inside loops – simple practical example
for ( var i in arrayObj) {
entity_list.onmouseup = function(event) {
parent.changeButtonState(this, event);
testing(arrayObj[i].ResourceId);
}
}
function testing(index){
return function() { getGoogleFiles(index); };
}
But when the element of "entity_list" is triggered, nothing happens.
I can't use let because the specific browser that I'm using returns a SyntaxError
SyntaxError: Block-scoped declarations (let, const, function, class) not yet supported outside strict mode
Thank you!

You need to use testing() to create the listener function, not something you call inside it.
for (var i in arrayObj) {
entity_list.onmouseup = testing(arrayObj[i].ResourceId, parent);
}
function testing(index, parent) {
return function(event) {
parent.changeButtonState(this, event);
getGoogleFiles(index);
};
}
But you wouldn't have to go through any of this if you use forEach() instead of a for loop, since it creates a new scope for obj in each iteration.
arrayObj.forEach(function(obj) {
entity_list.onmouseup = function(event) {
parent.changeButtonState(this, event);
testing(obj.ResourceId);
}
});

You can't use a var scoped variable here.
But you could assign the resourceId to a data attribute on the relative html element, so you can read it when the event fires.
var arrayObj = [{ResourceId: "test1"}, {ResourceId: "test2"}, {ResourceId: "test3"}];
var entity_list = document.getElementsByClassName("entity_list");
for ( var i in arrayObj) {
entity_list[i].dataset.resourceId = arrayObj[i].ResourceId;
entity_list[i].onmouseup = function(event) {
getGoogleFiles(this.dataset.resourceId);
}
}
function getGoogleFiles(resourceId) {
console.log(resourceId);
}
<span class="entity_list">entity list (item 1)</span>
<span class="entity_list">entity list (item 2)</span>
<span class="entity_list">entity list (item 3)</span>

Related

Remove event listener doesn't work as it should

I simply tried to addEventListener and removeEventListener to element, but it doesn't remove.
I suppose that the problem could be with parameters, but I used them to follow the DRY. So I could simply reuse it like nextSection.addEventListener('mouseover', showContent(event, nextSection)) and so on and so on so I do not need any if statements or stuff like that.
* EDIT *
I made some more examples of elements that I will be using. There’s a chance, that there will be event more. If I do not use parameter, there would be a lot more of functions. Also, there will be click instead of mouse events on mobile, so I need to remove them.
As I understand now, the problem is with return statement. If I use event instead of parameter and so event.target I get some weird bug.
const loginSection = document.querySelector('#js-login-section');
const searchSection = document.querySelector('#js-search-section');
const shoppingBagSection = document.querySelector('#js-shopping-bag-section');
const wishlistSection = document.querySelector('#js-wishlist-section');
function showContent(element) {
return () => {
const toggle = element.lastElementChild;
toggle.style.maxHeight = toggle.scrollHeight + 'px';
}
}
function hideContent(element) {
return () => {
const toggle = element.lastElementChild;
toggle.style.maxHeight = null;
}
}
/* Media queries - min width 992px */
loginSection.addEventListener('mouseover', showContent(loginSection));
loginSection.addEventListener('mouseout', hideContent(loginSection));
searchSection.addEventListener('mouseover', showContent(searchSection));
searchSection.addEventListener('mouseout', hideContent(searchSection));
shoppingBagSection.addEventListener('mouseover', showContent(shoppingBagSection));
shoppingBagSection.addEventListener('mouseout', hideContent(shoppingBagSection));
wishlistSection.addEventListener('mouseover', showContent(wishlistSection));
wishlistSection.addEventListener('mouseout', hideContent(wishlistSection));
/* Media queries - max width 992px */
loginSection.removeEventListener('mouseover', showContent(loginSection));
loginSection.removeEventListener('mouseout', hideContent(loginSection));
searchSection.removeEventListener('mouseover', showContent(searchSection));
searchSection.removeEventListener('mouseout', hideContent(searchSection));
shoppingBagSection.removeEventListener('mouseover', showContent(shoppingBagSection));
shoppingBagSection.removeEventListener('mouseout', hideContent(shoppingBagSection));
wishlistSection.removeEventListener('mouseover', showContent(wishlistSection));
wishlistSection.removeEventListener('mouseout', hideContent(wishlistSection));
Thank you in advance!
What is happening is that return () => {}; is returning a new function every time it's run. So every time you call one of your functions a new event handler is being created.
This means that the handler that is added is different to the one you're trying to remove.
To remedy this, I'd keep it simple:
const loginSection = document.querySelector('#js-login-section');
function showContent(e)
{
const toggle = e.currentTarget.lastElementChild;
toggle.style.maxHeight = toggle.scrollHeight + 'px';
}
function hideContent(e)
{
const toggle = e.currentTarget.lastElementChild;
toggle.style.maxHeight = null;
}
loginSection.addEventListener('mouseover', showContent);
loginSection.addEventListener('mouseout', hideContent);
loginSection.removeEventListener('mouseover', showContent);
loginSection.removeEventListener('mouseout', hideContent);
I'm not sure what you want to avoid repeating, so I can't advise on that, but I'm sure you'll figure it out.
const loginSection = document.querySelector('#js-login-section');
function showContent(event) {
var element = event.target;
return () => {
const toggle = element.lastElementChild;
toggle.style.maxHeight = toggle.scrollHeight + 'px';
}
}
function hideContent(event) {
var element = event.target;
return () => {
const toggle = element.lastElementChild;
toggle.style.maxHeight = null;
}
}
loginSection.addEventListener('mouseover', showContent);
loginSection.addEventListener('mouseout', hideContent);
loginSection.removeEventListener('mouseover', showContent);
loginSection.removeEventListener('mouseout', hideContent);
You must set in events method function without call. Element you can get from event event.target
In your code, I found the following errors,
param 'event' will be always undefined - the event should go as a parameter to inner function.
you don't need closure here - You can directly assign the function without creating an inner function and access the element with event.target or this
with your implementation, you should pass the same handler reference used in addEventListener to removeEventListener. So, you should store the handler in a variable and pass it to both addEventListener and removeEventListener
Solution:
if you don't know the handler name, you can use window.getEventListeners to do the magic,
window.getEventListeners returns a dictionary of events associated with the element.
function removeEventListener(el, eventName) {
if (!el) {
throw new Error('Invalid DOM reference passed');
}
const listeners = getEventListeners(el)[eventName] || [];
listeners.forEach(({
listener
}) => {
removeEventListener(eventName, listener);
});
}
function removeAllEventListener(el) {
if (!el) {
throw new Error('Invalid DOM reference passed');
}
const events = Object.entries(getEventListeners(el) || {});
events.forEach(([eventName, listeners]) => {
listeners.forEach(({
listener
}) => {
removeEventListener(eventName, listener);
});
});
}
// example
// remove mouseout event
removeEventListener(loginSection, 'mouseout');
// remove all event listeners
removeAllEventListener(loginSection);

Using 'this' inside a callback inside an object literal [duplicate]

This question already has answers here:
Closed 10 years ago.
Possible Duplicate:
Javascript prototype ‘this’ issue
I have an event listener which of course calls a method on an event. This method tries unsuccessfully to hold a reference to the holding objects this so that it can access other properties of the object.
There is a single comment denoting where the behavior is not understood. this_hold.Name is not accessible there as I thought it would be.
/*MUserExist
**
**
**
*/
$A.module({
Name: 'MUserExist',
S: {
ClientStorage: SClientStorage,
ComMessage: SComMessage,
ComText: SComText,
DynSma: SDynSma,
DynTwe: SDynTwe,
DynArc: SDynArc,
AniMorphLabel: SAniMorphLabel,
AniFlipPage: SAniFlipPage
},
E: {
but: $A('#ue_but')[0],
text: $A('#ue_go')[0],
form: $A('#ue_fo')[0],
check: $A('#ue_check')[0]
},
J: {
box: $('#ue_box')
},
init: function () {
var pipe = {},
this_hold = this;
this.J.box.draggable();
this.E.but.addEventListener("click", function () {
pipe = $A.definePipe(this_hold.Name);
$A.machine(pipe);
}, false);
this.E.text.addEventListener("keypress", this.enter, false);
this.S.AniMorphLabel.run(["ue_email",
"ue_email_lab",
"ue_go",
"ue_pass_lab"
]);
},
enter: function (event) {
var pipe = {},
this_hold = this;
if (event.keyCode === 13) {
pipe = $A.definePipe(this_hold.Name); // fails here what does 'this' point to?
$A.machine(pipe);
event.preventDefault();
}
},
pre: function (pipe) {
var form_elements = this.E.form.elements,
text_object = new this.S.ComText(form_elements);
pipe.enter = this.enter;
if ($A.Un.get('load') === '1') {
if (!text_object.checkFull()) {
pipe.type = 'empty';
return this.S.ComMessage.message(pipe);
}
if (!text_object.checkPattern('email')) {
pipe.type = 'email';
return this.S.ComMessage.message(pipe);
}
if (!text_object.checkPattern('pass')) {
pipe.type = 'pass';
return this.S.ComMessage.message(pipe);
}
}
pipe.page = text_object.getArray();
pipe.proceed = true;
pipe.page.remember = this.E.check.checked;
return pipe;
},
post : function (pipe) {
if (pipe.proceed === true) {
this.S.ComMessage.resetView('ue_email');
this.S.ComMessage.resetView('ue_go');
this.S.ClientStorage.setAll(pipe.server.smalls);
this.S.DynSma.run(pipe.server.smalls);
this.S.DynArc.run(pipe.server.arcmarks);
this.S.DynTwe.run(pipe.server.tweets);
this.S.AniFlipPage.run('ma');
} else {
return this.S.ComMessage.message(pipe);
}
}
});
this likely points to the DOM node from which the event was triggered. Have you tried writing this to the console to inspect it?
console.log(this);
this is the DOM object that generated the event. It is NOT your javascript object.
When you pass this.enter as the method for the event handler, the method enter does not stay bound to your object. If you want that to happen, you have to change your code to cause that to happen by doing something like this:
// save local copy of my object so I can refer to it in
// the anonymous function
var obj = this;
this.E.text.addEventListener("keypress", function(event) {obj.enter(event)}, false);
It is important to remember that this is set by the caller of a method/function. In this case the caller of the event handler is the event sub-system in the browser. It does not know what your object is and it's designed behavior is to set this to the DOM object that caused the event. So, if you want to call your obj.enter method, you can't just pass enter as the event handler. Instead, you make a separate function that gets called as the event handler and you then call obj.enter() from that using your object as the base so that this gets set correctly.
Another solution would be to use .bind() which also creates a stub function that binds the right this to a function call, but I don't use .bind() myself because it doesn't work in all older browsers.
Try to change how the event is being bound
this.E.text.addEventListener("keypress", this.enter, false);
to
var that = this;
this.E.text.addEventListener("keypress", function(event) {
that.enter(event);
}, false);

Classes in JavaScript using prototype

I have a problem, I want to create a JavaScript class:
function Calculatore(txt,elements) {
this.p= new Processor();
this.output=txt;
$(elements).click(this.clickHandler);
}
Calculatore.prototype.clickHandler = function() {
var element=$(this);
// Code Here
// "this" contains the element.
// But what if I want to get the "output" var?
// I tried with Calculatore.prototype.output but no luck.
}
So how can I solve this?
You can use jQuery's $.proxy:
function Calculatore(txt,elements) {
this.p= new Processor();
this.output=txt;
$(elements).click($.proxy(this.clickHandler, this));
}
Calculatore.prototype.clickHandler = function(event) {
var clickedElement = event.target;
alert(this.output);
}
Edited. Jason brought up a good point in the comments. It's probably better to use event.target which references only the element clicked, rather than elements which may reference an array of objects matching the selection.
You have a collision between this values. You currently don't have access to the instance because this has been set to the element inside a click handler.
You could make a proxy function to pass both the this value (the element) and the instance:
function Calculatore(txt,elements) {
this.p= new Processor();
this.output=txt;
var inst = this; // copy instance, available as 'this' here
$(elements).click(function(e) {
return inst.clickHandler.call(this, e, inst); // call clickHandler with
// 'this' value and 'e'
// passed, and send 'inst'
// (the instance) as well.
// Also return the return
// value
});
}
Calculatore.prototype.clickHandler = function(e, inst) {
var element = $(this);
var output = inst.output;
};

Erroneous behavior of local variables in closures

I am stuck at the following code. At first I'll describe the use-case: The function "addPreset" gets called with an instance of ColorGradient. When calling this.listController.addItem(...) a callback function named onSelect ist supplied, which gets called everytime the onSelect-event on the listController-item is triggered. What I wanted to do is wrapping the call to GLab.ColorSlider.applyColorGradient(...) into a new closure, so that the assigned value of addPreset's "cg" argument"* will be "caught" inside it. But it doesn't work.
PROBLEM: Now everytime addPreset is called, the value of cg (being passed with a call) will override all values that bad been assigned before. However, this.presetList holds always correct values (the ones I expected to be caught inside the closure-function. Even inserting an anonymous function for breaking the scope doesn't help.
Please help me. :-)
Thanks, so far
function addPreset(cg) {
if (!(cg instanceof ColorGradient)) {
throw new TypeError("PresetManager: Cannot add preset; invalid arguments received");
}
var newIndex = this.listController.addItem(cg.getName(), {
onSelect: (function(cg2) {
return function() {
// addPreset's scope should now be broken
GLab.ColorSlider.applyColorGradient(cg2);
console.log(cg2);
}
})(cg)
});
this.presetList[newIndex] = cg;
}
#bobince: of course you can.
the code snippet above is part of PresetManager.js and the listController is an instance of the class ListWrapper.js
http://code.assembla.com/kpg/subversion/nodes/GradientLab/lib-js/PresetManager.js
http://code.assembla.com/kpg/subversion/nodes/GradientLab/lib-js/ListWrapper.js
#Matt: cg is an instance of ColorGradient. A custom class of myself. Further more, it is assured, that always "valid" values are passed in as cg. (When you'd have a few minutes you can download the whole assembla repo as zip-archive. Unzip and test in FF > 3.5 with Firebug console enabled.)
Answer can be found in this question: Doesn't JavaScript support closures with local variables?
Someone please correct me if I am wrong, as I am still fairly new to JavaScript closures and scope. But it would seem to me that the wrapping anonymous function you have is simply there to provide a proper scoped variable/closure for the function it is returning. Could this be simplified as such?
function addPreset(cg) {
if (!(cg instanceof ColorGradient)) {
throw new TypeError("PresetManager: Cannot add preset; invalid arguments received");
}
var closured = cg;
var newIndex = this.listController.addItem(cg.getName(), {
onSelect: function() {
// addPreset's scope should now be broken
GLab.ColorSlider.applyColorGradient(closured);
console.log(closured);
}
});
this.presetList[newIndex] = cg;
}
Just want to tell you, that I finally solved my problem by myself. It cost me almost 2 days (in the sparetime) to puzzling it out, but I think its worth that. At least my code remained elegant and I definitely got the whole thing with closures. Let's have a look:
My faulty code
Part 1 of 2:
function addPreset(cg) {
if (!(cg instanceof ColorGradient)) {
throw new TypeError("PresetManager: blablabla");
}
// calls the function in Part 2
var newIndex = this.listController.addItem(cg.getName(), {
onSelect: (function(cg2) {
return function() {
// addPreset's scope should now be broken
GLab.ColorSlider.applyColorGradient(cg2);
console.log(cg2);
}
})(cg)
});
this.presetList[newIndex] = cg;
}
Part 2 of 2:
// The method being called by this.listController.addItem(..)
function addItem(caption, args) {
var _this = this,
currIndex,
id,
newItem
itemSelectCb = (!!args && typeof args.onSelect == "function") ?
args.onSelect :
undefined;
currIndex = this.numOfItems;
id = this.ITEM_ID_PREFIX + currIndex;
newItem = this.$itemTemplate
.clone()
.text(caption)
.attr("id", id)
.bind("click", function(e) {
e.stopPropagation();
if (typeof itemSelectCb != "undefined") {
itemSelectCb();
}
_this._onSelect($(".ListWrapperItem").index(this));
})
.appendTo(this.$container);
this.numOfItems = $("." + this.DEFAULT_ITEM_CLASS, this.$container).length;
return currIndex;
}
The fixed code
The bug was in Part 2; when calld jQuery's bind-method for adding an click-event-listener I used an anonymous function (= new closure), but referenced itemSelectCb inside; so the anonymous function's scope stayed "connected" to the one of addItem. Everytime I called addItem, an other value were assigned toitemSelectCb what lead to the unknown sideeffect, that all references to itemSelect inside previously created anonymous functions are pointing to that value. What meant, that the last assigned value, had been used by all anonymous function.
To "break" the scope, all I had to do was to modify the lines of Part 2 where the event-handler for jQuery's bind was created. The fixed code looks then like this:
function addItem(caption, args) {
var _this = this,
currIndex,
id,
newItem
itemSelectCb = (!!args && typeof args.onSelect == "function") ?
args.onSelect :
undefined;
currIndex = this.numOfItems;
id = this.ITEM_ID_PREFIX + currIndex;
newItem = this.$itemTemplate
.clone()
.text(caption)
.attr("id", id)
.bind("click", (function(itemSelectCb) {
return function(e) {
e.stopPropagation();
if (typeof itemSelectCb != "undefined") {
itemSelectCb();
}
_this._onSelect($(".ListWrapperItem").index(this));
}
})(itemSelectCb))
.appendTo(this.$container);
this.numOfItems = $("." + this.DEFAULT_ITEM_CLASS, this.$container).length;
return currIndex;
}

Can event handler defined within JavaScript object literal access itself?

I know I could do this with closures (var self = this) if object was a function:
click here
<script type="text/javascript">
var object = {
y : 1,
handle_click : function (e) {
alert('handling click');
//want to access y here
return false;
},
load : function () {
document.getElementById('x').onclick = this.handle_click;
}
};
object.load();
</script>
The simplest way to bind the call to handle_click to the object it is defined in would be something like this:
var self=this;
document.getElementById('x').onclick =
function(e) { return self.handle_click(e) };
If you need to pass in parameters or want to make the code look cleaner (for instance, if you're setting up a lot of similar event handlers), you could use a currying technique to achieve the same:
bind : function(fn)
{
var self = this;
// copy arguments into local array
var args = Array.prototype.slice.call(arguments, 0);
// returned function replaces first argument with event arg,
// calls fn with composite arguments
return function(e) { args[0] = e; return fn.apply(self, args); };
},
...
document.getElementById('x').onclick = this.bind(this.handle_click,
"this parameter is passed to handle_click()",
"as is this one");
So, the event handler part wires up just fine (I tested it myself) but, as your comment indicates, you have no access to the "y" property of the object you just defined.
This works:
var object = {
y : 1,
handle_click : function (e) {
alert('handling click');
//want to access y here
alert(this.y);
return false;
},
load : function () {
var that = this;
document.getElementById('x').onclick = function(e) {
that.handle_click(e); // pass-through the event object
};
}
};
object.load();
There are other ways of doing this too, but this works.
I see how to do it with Jason's latest one. Any way to do it without the anonymous function?
We can directly pass an object with a handler method thanks to AddEventListener, and you will have access to its attributes:
http://www.thecssninja.com/javascript/handleevent
Hope this will help those who, like me, will look for this topic some years after!

Categories