Event.observe in a loop and variables - javascript

To put things in context, I'm loading a list of items via Ajax, creating a div with main info for each one and want to display details on page when clicking on it. So I have that code in my onSuccess :
items = transport.responseText.evalJSON(); // my list of objects that contains all the details I'll need for that page
for (var itemID in items)
{
newDiv = ... // Creating my div with main infos
$('myDiv').appendChild(newDiv);
// More code to make everything look pretty and that works fine
Event.observe(newDiv, 'click', function() { loadItem(itemID); });
}
loadItem is my function that will display all the item details. And my problem is that itemID isn't replace by its value when creating the observe event, so it always returns the same ID for all items.
Any idea how I can fix that ? I checked bind on prototype doc, that seemed to be made for those cases, but probably didn't get it, since it wouldn't work for me.

For a minimal-impact fix, replace your Event.observe line with this:
Event.observe(newDiv, 'click', loadItem.curry(itemID));
Explanation:
In your original code, the event handler functions you're creating close over (have an enduring reference to) the itemID variable, and so will use the value of that variable at of when the event handler is called, not as of when you assign it to the event. That value will be the last value that itemID has in the loop — for all of the handler functions. More about closures here.
With the minimal-impact revised code, we use Prototype's curry function, which will create a function for you that, when called, will call the underlying function with the arguments you gave curry. (The name is from mathematics; Haskell Curry came up with the technique, though there are arguments he wasn't the first to do so.) We could do the same thing ourselves:
items = transport.responseText.evalJSON(); // my list of objects that contains all the details I'll need for that page
for (var itemID in items)
{
newDiv = ... // Creating my div with main infos
$('myDiv').appendChild(newDiv);
// More code to make everything look pretty and that works fine
Event.observe(newDiv, 'click', prepLoadItem(itemID));
}
function prepLoadItem(id) {
return function() {
loadItem(id);
};
}
...but because Prototype has a general-purpose function for it, we don't have to.
Off-topic: Is items an array? If not, ignore this off-topic comment. If so, don't use for..in to loop through it, or at least, not unless you take some precautions the code above doesn't to do it properly. Details here, but for..in is not for looping through the indexes of an array; it's for looping through the properties of an object. Array objects may well have properties other than array indexes (and in fact, if you're using Prototype, they do.)

Related

addEventListener is not a function even though array returns nodes?

I realize there are other questions with a similar title, but I'm running into something very strange.
In the code below I get an array of nodes and iterate through the array to add events:
activateButtons = () => {
let catButtons = document.getElementsByClassName('catalogItem')
for(i in catButtons){
console.log(catButtons[i])
catButtons[i].addEventListener('click', () => {
catButtons[i].classList.toggle('active')
if(catButtons[i].nextElementSibling){
catButtons[i].nextElementSibling.classList.toggle('show')
}
})
}
}
I get the same result whether I use ('click', e => or ('click', () =>. A later function uses ('click', () => and works just fine when a button is created.
I'm logging the array there to make sure that it's being captured (activateButtons is called after another function that constructs a dom element, that part works fine. In the log I get:
Based on other answers, I'm doing this correctly - I'm assigning event listeners to the nodes IN the array, not the array itself. The array is clearly being assembled. So why am I getting this error?
Elsewhere in the code, when a new button is initially created the function includes almost the exactly same code, except that I make the button (document.createElement) and then assign it immediately to the button, rather than requiring an array.
On load, the buttons already made are re-instantiated from a json file, and when I tried to addEventListener at that point, I didn't get the error, but also didn't get the event, so instead I decided to add the events all at once after the buttons are created. What am I missing here?
for..in iterates over all enumerable properties anywhere in the prototype chain. For an HTMLCollection, this results in, in addition to numeric properties:
for (const i in document.getElementsByClassName('catalogItem')) {
console.log(i);
}
length, item, namedItem - the values on each of those properties are not elements (so calling addEventListener on them fails).
Use for..of instead, to invoke the HTMLCollection's iterator, which gives you only the elements:
for (const button of catButtons) {
button.addEventListener('click', () => {
// ...
});
}
Since you're adding listeners in a loop, make sure to declare the iteration variable (i or button) - with const; using for(i results in there only being one global binding for i, which will result in problems if you find you want to examine the variable inside the listener. (See the closure inside loops question)

Simple event system using JavaScript (ES5) - JS Custom Event

Instructions:
make this code work without modifying snippet it in any way.
Use only plain old JavaScript and no third-party libraries.
Write new code that enables the code below to work properly.
Hint: Feel free to extend native objects... even though it's typically a bad practice.
// Start with an object, any object
var myObject = {};
// Register an event on your object using
// an `on` method
myObject.on('myEvent', function(data) {
// Log the data passed to the callback
console.log(data);
});
// Trigger the event using a `trigger` method.
// Include some data when you trigger the event.
myObject.trigger('myEvent', {
company: 'ABC Corp',
location: 'WTC Bangalore, IN',
website: 'http://abc.co'
});
// Register a different event
myObject.on('yourEvent', function() {
console.log('yourEvent fired');
});
// Trigger the new event
myObject.trigger('yourEvent');
// Trigger all existing events using a special
// "star" identifier.
myObject.trigger('*');
// Remove one event by name
myObject.off('myEvent');
// Since we've removed the event, this should
// do nothing
myObject.trigger('myEvent');
// Remove all existing events
myObject.off();
// Since we've removed all events, this should
// do nothing
myObject.trigger('*');
Everything else went well. I'm unable to get "arguments" while implementing myObject.trigger("*"); unable to read arguments object / parameters while implementing "*" and hence throw undefined.
My JSFiddle
Disclaimer
I obviously dont know what school you go to or anything, but please don't fool yourself trying to fool your teachers. With a few simple questions they'll know if you understand the material or not, and if you show up with a good answer but no knowledge to back it up, they will know what's up. I'm not accusing you of this, just a friendly word of advice of someone who has had good connections with his teachers after graduating last year ;)
So, how do we do this? Basically, you will have to add some functionality to the prototype of object, at least if you want this to affect all objects made afterwards. You can always create your own class and add the function to that prototype if you only want that class to have this functionality.
We need 3 functions added to the prototype, on, off and trigger of course. On top of that we add one extra property called events, initially an empty object.
You can look at the raw code for all these in the jsfiddle, I will only go through the structure and logic of the code here.
events will hold all the handlers (functions) associated with each event. When adding an event for the first time, we add a eventName property to the events object, the value for this property is initially an empty array.
on will find (or create) the array linked to eventName in events, and push the function into the array (note we do not call the function at this time, we simply store the reference to the function in the array).
off will iterate the array of eventName, and if it finds the same function (note the ===), remove it from the array.
trigger will iterate the array of eventName and call each function. Note that the function is called with the this keyword in the function set to the object, and with the same parameters as the trigger function was called (except eventName, the first parameter, which is filtered out). Yes that means you can pass as many parameters as you want to trigger(), and they will all be passed to each handler.
I won't go into detail what things like splice, slice, ===, arguments and apply do exactly, I'm sure you can find more and better information about that elsewhere on the world wide interwebs.
There's a lot more you can do for this, like making the events object invisible through some nice uses of scoping, but that wasn't part of the question so I didn't bother with that.
If you have any more questions after looking through this, feel free to ask. I also didn't test it extensively so if you find any bugs, let me know.
EDIT: I didn't read through the comments at first, but I now also added support for the '*' wildcard. Basically the functions now check for the wildcard and will iterate all eventNames on the event object when removing or triggering. You can also remove all functions for an event by not giving a function or by giving the same wildcard, but with an eventName.
EDIT2: had some bugs running the teacher's code, realized I forgot to check for hasOwnProperty while iterating. Look that one up, it's very important when working with prototypes!
I now put in the teacher's code in my jsfiddle, to show you that it works :)
jsfiddle with own code
jsfiddle with teacher code
EDIT3 - about the 'undefined' log.
The teacher's code calls .trigger 5 times, and you should see 4 console logs and as far as I can tell, they are all correct.Let me run through each trigger, and the subsequent console logs.
You add a handler to myEvent, which logs the first parameter
You trigger myEvent, with parameter => The parameter (the object), is
logged.
You add a handler to yourEvent, which logs a hardcoded
string.
You trigger yourEvent, no parameter => The hardcoded string is logged'
You trigger * with no parameter, all handlers run => undefined is logged, since no parameters were given, data in myEvent's handler is undefined. The hardcoded string is also logged
You remove the myEvent handler, trigger myEvent and confirm no functions are called
You remove all event handlers, trigger * and confirm no functions are called from any events.
I honestly don't know what you expected to happen on step 5, since you give no parameter, the data is assigned undefined, that's intended behaviour.
If you want to merge the data given in step 2 so it remains on the object, then instruct so in your handler. (for example, iterate all properties of data and add them to this, then log this). Right now you simply pass it data, it gets logged, and then thrown away. You can also add a parameter in step 5, and then all handlers will receive it (including the yourEvent handlers, but that one doesn't assign nor use it).
document.getElementById("myBtn").addEventListener("click", displayDate);

Adding onclick properties to each item in DOM object list in Javascript

I'm trying to move away from jQuery for my everyday site functionality, and I'm having a little bit of trouble with the onclick event. I'd like to put together a function like jQuery's .click(), but simply using document.getElementsByTagName and adding a func onclick won't work.
The question then is how would one add a single function to fire onclick to all elements in the list object returned by querying document.getElementsByTagName('h4')
EDIT: Just in case someone finds this and would like some code, here's what I did:
var headings = document.getElementsByTagName('h4')
for (var g in headings) {
headings[g].onclick = function() {
//code
}
}
You need to loop through the list and pass the event to each item.
I think there is no simpler way to do this, expect you need a library like jQuery or you write your own eventManager...

Passing values from Greasemonkey sandbox into anonymous functions

This is my first question, probably very silly indeed :)
I have a selection of values in an array, returned from GM_listValues().
As I loop over the collection, I want to dynamically create buttons that call a function to delete the stored value, and reload the page.
deleteB.addEventListener("click", function() {deleteTrip(names[i]);pageSelect();}, false);
Above is the line I am using to attach the event to the button (deleteB). However, when I press the button, javascript tries to access the array of listValues (names) with the count variable (i). Naturally, this will not exist, as the loop is now done, and names is not global anyway.
What I want to know is if there is a way to copy the string value of names[i] while I am creating the function in the button, so as to not need a reference to names[i] in the code.
I know this is probably a really simple answer, but its got me stumped, this is some of my first work with javascript.
Thanks in advance.
Use a closure to remember the value;
function createDeleteFunc(name) {
return function(){deleteTrip(name);pageSelect();}
}
for() {
...
deleteB.addEventListener("click", createDeleteFunc(names[i]), false);
...
}
The problem is that all functions you create reference the same i variable. When they are called, they try to delete names[i], but i is now equal to names.length so it doesn't work.
The solution is to make a separate reference to names[i] for each function. This is usually done with a closure (à-la Paul's answer)

How to assign event callbacks iterating an array in javascript (jQuery)

I'm generating an unordered list through javascript (using jQuery). Each listitem must receive its own event listener for the 'click'-event. However, I'm having trouble getting the right callback attached to the right item. A (stripped) code sample might clear things up a bit:
for(class_id in classes) {
callback = function() { this.selectClass(class_id) };
li_item = jQuery('<li></li>')
.click(callback);
}
Actually, more is going on in this iteration, but I didn't think it was very relevant to the question. In any case, what's happening is that the callback function seems to be referenced rather than stored (& copied). End result? When a user clicks any of the list items, it will always execute the action for the last class_id in the classes array, as it uses the function stored in callback at that specific point.
I found dirty workarounds (such as parsing the href attribute in an enclosed a element), but I was wondering whether there is a way to achieve my goals in a 'clean' way. If my approach is horrifying, please say so, as long as you tell me why :-) Thanks!
This is a classic "you need a closure" problem. Here's how it usually plays out.
Iterate over some values
Define/assign a function in that iteration that uses iterated variables
You learn that every function uses only values from the last iteration.
WTF?
Again, when you see this pattern, it should immediately make you think "closure"
Extending your example, here's how you'd put in a closure
for ( class_id in classes )
{
callback = function( cid )
{
return function()
{
$(this).selectClass( cid );
}
}( class_id );
li_item = jQuery('<li></li>').click(callback);
}
However, in this specific instance of jQuery, you shouldn't need a closure - but I have to ask about the nature of your variable classes - is that an object? Because you iterate over with a for-in loop, which suggest object. And for me it begs the question, why aren't you storing this in an array? Because if you were, your code could just be this.
jQuery('<li></li>').click(function()
{
$(this).addClass( classes.join( ' ' ) );
});
Your code:
for(class_id in classes) {
callback = function() { this.selectClass(class_id) };
li_item = jQuery('<li></li>')
.click(callback);
}
This is mostly ok, just one problem. The variable callback is global; so every time you loop, you are overwriting it. Put the var keyword in front of it to scope it locally and you should be fine.
EDIT for comments: It might not be global as you say, but it's outside the scope of the for-loop. So the variable is the same reference each time round the loop. Putting var in the loop scopes it to the loop, making a new reference each time.
This is a better cleaner way of doing what you want.
Add the class_id info onto the element using .data().
Then use .live() to add a click handler to all the new elements, this avoids having x * click functions.
for(class_id in classes) {
li_item = jQuery('<li></li>').data('class_id', class_id).addClass('someClass');
}
//setup click handler on new li's
$('li.someClass').live('click', myFunction )
function myFunction(){
//get class_id
var classId = $(this).data('class_id');
//do something
}
My javascript fu is pretty weak but as I understand it closures reference local variables on the stack (and that stack frame is passed around with the function, again, very sketchy). Your example indeed doesn't work because each function keeps a reference to the same variable. Try instead creating a different function that creates the closure i.e.:
function createClosure(class_id) {
callback = function() { this.selectClass(class_id) };
return callback;
}
and then:
for(class_id in classes) {
callback = createClosure(class_id);
li_item = jQuery('<li></li>').click(callback);
}
It's a bit of a kludge of course, there's probably better ways.
why can't you generate them all and then call something like
$(".li_class").click(function(){ this.whatever() };
EDIT:
If you need to add more classes, just create a string in your loop with all the class names and use that as your selector.
$(".li_class1, .li_class2, etc").click(function(){ this.whatever() };
Or you can attach the class_id to the .data() of those list items.
$("<li />").data("class_id", class_id).click(function(){
alert("This item has class_id "+$(this).data("class_id"));
});
Be careful, though: You're creating the callback function anew for every $("<li />") call. I'm not sure about JavaScript implementation details, but this might be memory expensive.
Instead, you could do
function listItemCallback(){
alert("This item has class_id "+$(this).data("class_id"));
}
$("<li />").data("class_id", class_id).click(listItemCallback);

Categories