How do I bake unique accessible/passable values into multiple displayed buttons? - javascript

Backstory:
• Varying dynamic items (buttons) will be generated and displayed in a single div.
• Each button is created from a unique object with a unique ID value.
Problem:
How do I get each generated and displayed button to retain, and then pass along when clicked, its unique "id"?
All of my efforts so far have gotten me results of "undefined" or displaying only the last generated id value, regardless of what button is clicked. Also things that target DOM elements don't seem to help as each of my unique items will not be inside it's own element. Rather just listed out in a single element.
As far as ideas/answers I am after straightforward/readability vs. speed/efficiency. I am also trying to keep as much of my functionality on the javascript side and rely on HTML for as little as possible beyond "displaying" things.
The following code is working as expected for me sans my question:
var allItems = [
{id:1, name:"Space Gem", power:100},
{id:14, name:"Time Gem", power:200},
{id:22, name:"Reality Gem", power:300}
];
var map = {
tile: [
{id:22},
{id:1}
]
}
onTile();
function onTile() {
for ( var i = 0; i < map.tile.length; i++ ) {
var itemId = map.tile[i].id;
for (var j = 0; j < allItems.length; j++) {
if (itemId === allItems[j].id) {
var itemName = allItems[j].name;
var button = document.createElement("button");
button.innerHTML = itemId + " " + itemName;
document.getElementById("tile_display").appendChild(button);
button.addEventListener ("click", get, false);
}
}
}
}
function get(itemId) {
alert ("You clicked button with ID: " + itemId);
}

The only problem I see is that you are passing the same event listener to each newly-created button. And what is more, you are passing the get function but not specifying an argument - which means that itemId will always be undefined when the function runs in response to a click. (I realise now this isn't true - itemId instead will refer to the Event object corresponding to the click event that's just happened - but this is no use to you in this case.)
So all you need to do, I think, is change:
button.addEventListener ("click", get, false);
to:
button.addEventListener ("click", function() {get(itemId);}, false);
EDIT: so this solves the "undefined" problem. But as you noticed, you are getting "id: 1" for both buttons. This is due to the fact that the event listener is a "closure" over its enclosing scope, which here is the onTile function. This means that, when you click the button and the event listener runs, it looks up the value of itemId, which it still has access to even though that scope would otherwise have been destroyed. But there is only one itemId in that scope, and it has whichever value it had when the function finished executing (here 1) - the same value for each event listener.
The simplest fix by far, assuming you are running in ES6-supporting browsers (which these days is all of them, although it always amazes me how many are still using IE which doesn't support it), is simply to change var ItemId = ... to let ItemId = .... Doing this gives ItemId a new scope, that of the loop itself - so you get a different value "captured" each time through the loop - exactly as you want.
In case you do need to support pre-ES6 browsers, you can perform the same "trick" without let, by enclosing the whole body of the outer for loop in a function (this creates a new scope each time), and then immediately invoking it, like this:
function onTile() {
for ( var i = 0; i < map.tile.length; i++ ) {
(function() {
var itemId = map.tile[i].id;
for (var j = 0; j < allItems.length; j++) {
if (itemId === allItems[j].id) {
var itemName = allItems[j].name;
var button = document.createElement("button");
button.innerHTML = itemId + " " + itemName;
document.getElementById("tile_display").appendChild(button);
button.addEventListener ("click", function()
{get(itemId);},
false);
}
}
})();
}
}
function get(itemId) {
alert ("You clicked button with ID: " + itemId);
}
Javascript closures, and in particular how they interact with loops like this, are a tricky topic which has caught many out - so there are loads of SO posts about it. JavaScript closure inside loops – simple practical example is an example, with the answer by woojoo66 being a particularly good explanation.

All that ever needs to happen here is to use the onClick = function() {} property for the newly created button and directly specify the itemId there like so:
button.onclick = function() {
get(itemId);
}
You can easily implement this in a little function like make_button(itemId) { } (see below)
make_button(1);
make_button(2);
make_button(3);
make_button(4);
make_button(5);
function make_button(itemId) {
var button = document.createElement("button");
button.onclick = function() {
get(itemId);
}
button.innerHTML = "button " + itemId;
document.getElementById("mydiv").appendChild(button);
}
function get(_itemId) {
alert("You picked button " + _itemId);
}
<div id="mydiv">
</div>
A much easier way to do this would be to do something like this:
var allItems = [{
id: 1,
name: "Space Gem",
power: 100
},
{
id: 14,
name: "Time Gem",
power: 200
},
{
id: 22,
name: "Reality Gem",
power: 300
}
];
var map = {
tile: [{
id: 22
},
{
id: 1
}
]
}
onTile();
function onTile() {
for (var i = 0; i < map.tile.length; i++) {
var itemId = map.tile[i].id;
/* filter out only items in allItems[] that have id === itemId */
var items = allItems.filter(item => item.id === itemId);
/* loop through those few items and run function make_button */
items.forEach(function(item) {
make_button(item.id, item.name);
});
}
}
/* avoid the use of function names such as 'get' 'set' and other commonly used names as they may conflict with other scripts or native javascript */
function make_button(itemId, itemName) {
var button = document.createElement("button");
button.innerHTML = itemId + " " + itemName;
button.onclick = function() {
get_clicked_tile(itemId); // changed name from 'get'
};
document.getElementById("tile_display").appendChild(button);
}
function get_clicked_tile(itemId) {
alert("You clicked button with ID: " + itemId);
}
<div id="tile_display"></div>

Related

How to Autorun an onclick event

sorry am still learning JavaScript, read W3Cschools javascript and Jquery but there is a lot they don't teach.
I am studying animation at the moment, how do I auto start this rather then wait for someone to click (event listener), I've attempted turning it into a function but I must be doing it wrong, also 1 more what does (Idx) mean, I understand (id) is Html ID element but not sure Idx, not easy to find on google. to read, event listener starts at 5th line from the bottom, and the shuffle cards is 6th line from top (not sure if that helps), original code is located here http://www.the-art-of-web.com/javascript/css-animation/ thanks for any help.
Regards. William.
var cardClick = function(id)
{
if(started) {
showCard(id);
} else {
// shuffle and deal cards
card_value.sort(function() { return Math.round(Math.random()) - 0.5; });
for(i=0; i < 16; i++) {
(function(idx) {
setTimeout(function() { moveToPlace(idx); }, idx * 100);
})(i);
}
started = true;
}
};
// initialise
var stage = document.getElementById(targetId);
var felt = document.createElement("div");
felt.id = "felt";
stage.appendChild(felt);
// template for card
var card = document.createElement("div");
card.innerHTML = "<img src=\"/images/cards/back.png\">";
for(var i=0; i < 16; i++) {
var newCard = card.cloneNode(true);
newCard.fromtop = 15 + 120 * Math.floor(i/4);
newCard.fromleft = 70 + 100 * (i%4);
(function(idx) {
newCard.addEventListener("click", function() { cardClick(idx); }, false);
})(i);
felt.appendChild(newCard);
cards.push(newCard);
I've gone through your code and added comments to try and help explain what is going on in this file:
//Declare card click function. Takes one parameter (id)
var cardClick = function(id){
if(started) {
showCard(id);
} else {
// shuffle and deal cards
card_value.sort(function() {
return Math.round(Math.random()) - 0.5;
});
for(i=0; i < 16; i++) {
(function(idx) {
setTimeout(function() {
moveToPlace(idx);
}, idx * 100);
})(i);
}
started = true;
}
};
// initialise
//set stage as reference to some element
var stage = document.getElementById(targetId);
//append a div with ID "felt" to the stage element
var felt = document.createElement("div");
felt.id = "felt";
stage.appendChild(felt);
// template for card
//declare card variable as a div with some html content
var card = document.createElement("div");
card.innerHTML = "<img src=\"/images/cards/back.png\">";
//Loop from 0 to 16, where i = current value
for(var i=0; i < 16; i++) {
//create a copy of the card made earlier
var newCard = card.cloneNode(true);
//apply some attributes to the new card
newCard.fromtop = 15 + 120 * Math.floor(i/4);
newCard.fromleft = 70 + 100 * (i%4);
//Create and run an anonymous function.
//The function takes one parameter (idx)
//The function is called using (i) as (idx)
(function(idx) {
//add click handler to the card element that triggers the card click
//function with parameter (idx)
newCard.addEventListener("click", function() { cardClick(idx); }, false);
})(i);
//add new card to the stage
felt.appendChild(newCard);
//add new card to an array of cards
cards.push(newCard);
} //end for loop (I added this. It should be here)
how do I auto start this rather then wait for someone to click
The way I would do it, is add a manual click event after the for loop that targets the first card that has the event handler. Because there is no ID set on the cards, I would try using the array that the cards are added to. Assuming that the cards array was empty when we started:
cards[0].click();
If that doesn't work, I would try targeting the item in the DOM. We know that each card is added to the end of div#felt. So, if we can target the first div inside felt, we should be able to trigger the click event on it.
document.getElementByID("felt").firstChild.click();
what does (Idx) mean
I'm hoping the comments help explain this. It looks like the variable idx is just used as an extended reference of i. Inside a for loop, the writer creates a function that takes one parameter (idx). The for loop has a variable (i) that increases by one for each instance of the loop. Each time the loop happens, i is passed into function as idx.
I hope that helps to get you an understanding of how this code works.

How to add event listeners to an array of objects

I have an array of objects (specifically easelJS images) - something like this:
var imageArray = new Array;
gShape = new createjs.Shape();
// shape is something
imageArray.push(gShape);
What I want to do is have an event listener instead of:
gShape.addEventListener("click", function() {alert"stuff"});
I want the program to know specifically which region is clicked so that I can send an alert box in the following way:
imageArray[index].addEventListener("click", function(){
alert " you clicked region number " + index}
Sure. You can just use a closure to save the index of that iteration. Otherwise there are shared by the same function scope and will give you the value of the same iteration. Creating a separate function for each will save the state of that inside the function.
var imageArray = new Array;
gShape = new createjs.Shape();
// shape is something
imageArray.push(gShape); // Dumped all the objects
for (var i = 0; i < imageArray.length; i++) {
(function(index) {
imageArray[index].addEventListener("click", function() {
console.log("you clicked region number " + index);
})
})(i);
}
or better
for(var i = 0; i < imageArray.length; i++) {
imageArray[i].addEventListener("click", bindClick(i));
}
function bindClick(i) {
return function() {
console.log("you clicked region number " + i);
};
}
ES6 to the rescue
let imageArray = [];
gShape = new createjs.Shape();
// shape is something
imageArray.push(gShape); // Dumped all the objects
for (let i = 0; i < imageArray.length; i++) {
imageArray[i].addEventListener("click", function() {
console.log("you clicked region number " + i);
});
}
Using the let keyword creates a block scoping for the variable in iteration and will have the correct index when the event handler is invoked.
Something like this should work:
for (var i = 0 ; i < imageArray.length ; ++i) {
function(index) {
imageArray[index].addEventListener("click", function() {
alert ("You clicked region number: " + index");
});
} ( i);
}
The reason it works is because it creates a closure that holds the value of index that will be shown in the alert message. Each time through the loop creates another closure holding another value of index.
//gShape must be an array of HTMLElement
gShape.forEach(element => element.addEventListener("click", function () {
// this, refers to the current element.
alert ("You clicked region number: " + this.getAttribute('data-region'));
}));
Sure, a closure is the solution, but since he's got Ext loaded he might as well use it and get some very readable code. The index is passed as the second argument to Ext.Array.each (aliased to Ext.each).
Ext.each(imageArray, function(gShape, index) {
gShape.addEventListener("click", function() {
alert("You clicked region number " + index);
});
});
This is what I'm using for div id's:
var array = ['all', 'what', 'you', 'want'];
function fName () {
for (var i = 0; i < array.length; i++)
document.getElementById(array[i]).addEventListener('click', eventFunction);
};
Good Luck!
A simple way to do this, is by calling a querySelectorAll() on all
the elements and using a loop to iterate and execute a function with the data of that specific array index once the EventListener is
triggered by the element clicked.
Snippet
Retrieving the id attribute of the clicked element
document.querySelectorAll('li').forEach(element => {
element.addEventListener('click', () => {
console.log(element.getAttribute('id'))
})
})
li{cursor:pointer}
<ul>
<li id="id-one">One</li>
<li id="id-two">Two</li>
<li id="id-three">Three</li>
<li id="id-four">Four</li>
</ul>

How to Associate an Object with a DOM Element

I have a master object in my JS setup, i.e.:
var myGarage = {
cars: [
{
make: "Ford",
model: "Escape",
color: "Green",
inuse: false
},
{
make: "Dodge",
model: "Viper"
color: "Red",
inuse: true
},
{
make: "Toyota",
model: "Camry"
color: "Blue",
inuse: false
}
]
}
Now I loop over my cars and put them in a table. In the table I also have a button that lets me toggle the car as "in use" and "not in use".
How can I associate the DOM Element of every row with its corresponding car, so that if I toggle the "inuse" flag, I can update the master object?
You can actually attach an object directly to a node:
var n = document.getElementById('green-ford-escape');
n.carObject = myGarage.cars[0];
n.onclick = function() {
doSomethingWith(this.carObject);
}
For the same of removing ambiguity, in some cases, it's more clear write the above event handler to refer to n instead of this:
n.onclick = function() {
doSomethingWith(n.carObject);
}
You can also refer directly to the object from the attached event:
var n = document.getElementById('green-ford-escape');
n.onclick = function() {
doSomethingWith(myGarage.cars[0]);
}
In the latter case, myGarage does not have to be global. You can do this and expect it to work correctly:
(function(){
var myGarage = { /* ... etc ... */ };
var n = document.getElementById('green-ford-escape');
n.onclick = function() {
doSomethingWith(myGarage.cars[0]);
}
})();
The node's event function will "hold onto" the local variable correctly, effectively creating a private variable.
You can test this in your Chrome/FF/IE console:
var o = {a: 1};
var n = document.createElement('div');
n.innerHTML = "click me";
n.data = o;
n.onclick = function() { n.data.a++; console.log(n.data, o); }
document.body.appendChild(n);
You should see the console log two identical objects with each click, each with incrementing a values.
Beware that setting n.data to a primitive will not create a reference. It'll copy the value.
I'd suggest considering addEventListener, and a constructor that conforms your objects to the eventListener interface.
That way you can have a nice association between your object, your element, and its handlers.
To do this, make a constructor that's specific to your data.
function Car(props) {
this.make = props.make;
this.model = props.model;
// and so on...
this.element = document.createElement("div"); // or whatever
document.body.appendChild(this.element); // or whatever
this.element.addEventListener("click", this, false);
}
Then implement the interface:
Car.prototype.handleEvent = function(e) {
switch (e.type) {
case "click": this.click(e);
// add other event types if needed
}
}
Then implement your .click() handler on the prototype.
Car.prototype.click = function(e) {
// do something with this.element...
this.element.style.color = "#F00";
// ...and the other properties
this.inuse = !this.inuse
}
So then you can just loop over the Array, and make a new Car object for each item, and it'll create the new element and add the listener(s).
myGarage.cars.forEach(function(obj) {
new Car(obj)
})
You can use HTML5 data-* attribute to find out which row it is. You must be doing something like this
var table = $('<table>'); // Let's create a new table even if we have an empty table in our DOM. Simple reason: we will achieve single DOM operation (Faster)
for (var i=0; i<myGarbage.cars.length; i++) {
// Create a new row and append to table
var tr = $('<tr>').appendTo(table);
var carObject = myGarbage.cars[i];
// Traverse the JSON object for each car
for (var key in carObject) {
// Create other cells. I am doing the last one
var td = $('<td>').appendTo(tr);
var button = $('<button>').attr('data-carId', i).addClass('toggle-inuse').appendTo(td);
}
}
// If en ampty table awaits me in DOM
$('#tableId').html(table.html());
Now we will add event listener on button :-
$('.toggle-inuse').click(function() {
var i = $(this).data('carId');
myGarbage.cars[i].inuse = !myGarbage.cars[i].inuse; //Wow done
}
Try this out !!
You'll want some sort of ID or distinct row in your information, else you'll have to rely on the array index to do this. Either way you'll want to store the data using data attributes.
So when you loop through:
for (var i = 0, l = array.length; i < l; i++) {
var div = '<tr data-car="' + JSON.stringify(array[i]) + '" data-index="' + i + '"><td></td></tr>'
}
And on your click event:
$('button').click(function() {
var carIndex = $(this).closest('tr').attr('data-index');
var carData = $(this).closest('tr').attr('data-car');
if (carData) carData = JSON.parse(carData);
myGarage.cars[carIndex].inUse = true;
})
If you bind the data to the DOM, you may not even need to update the actual JS data. Could go over each row in the table and re-create the data-object you created the table from.

Scoping issue when attempting to pass variable into object's method constructor within a foreach loop

I understand what's happening in the following block of code, but unsure of how to fix it:
for (var i = 0; i < songs.length; i++){
var listItem = $('<li/>').appendTo(songList);
var song = songs[i];
var link = $('<a/>', {
id: song.id,
href: '#' + song.id,
text: song.name,
contextmenu: function(e){
var contextMenu = new ContextMenu(song);
contextMenu.show(e.pageY, e.pageX);
//Prevent default context menu display.
return false;
}
}).appendTo(listItem);
}
In this scenario, song, when accessed inside of the contextmenu method, will always be the last item iterated over in the foreach loop. I'm wondering how to make it so that there is a 1:1 relationship between links and their defining songs.
I tried assigning the song object as a property of the link object (accessing via this.song), but it comes up as undefined.
for (var i = 0; i < songs.length; i++){
// note this
(function(i) {
var listItem = $('<li/>').appendTo(songList);
var song = songs[i];
var link = $('<a/>', {
id: song.id,
href: '#' + song.id,
text: song.name,
contextmenu: function(e){
var contextMenu = new ContextMenu(song);
contextMenu.show(e.pageY, e.pageX);
//Prevent default context menu display.
return false;
}
}).appendTo(listItem);
// and this
}(i));
}
See this duplicate for an explanation.
The other answer is the better approach--declaring the object out of the scope of the for loop. But here's how to tackle the problem if declaring the object must be declared in the for loop:
song is declared up one level of scope from your contextMenu function. So whenever contextMenu executes, it will look up the scope chain until it finds the variable, by which point it will always find the last instantiated song. You need to get a copy of song available to the contextMenu, which is also tricky because objects are passed by reference in JavaScript. You can use a self-invoked function in combination with $.extend to make a copy of song (renamed newSong in my example) and solve this problem:
for (var i = 0; i < songs.length; i++){
var listItem = $('<li/>').appendTo(songList);
var song = songs[i];
var link = $('<a/>', {
id: song.id,
href: '#' + song.id,
text: song.name,
contextmenu: (function(song){
var newSong = $.extend(true,{},song);
return function(e) {
var contextMenu = new ContextMenu(newSong);
contextMenu.show(e.pageY, e.pageX);
//Prevent default context menu display.
return false;
};
})(song);
}).appendTo(listItem);
}

In JQGrid add onclick event on img

I'm very new to JQuery, and I'm having some trouble
function Clients(guid)
{
var that = this;
this.guid = guid;
this.container = $("#Clients_" + that.guid);
this.LoadClients = function () {
var ids = that.container.find("#clients-tbl").getDataIDs();
for (var i = 0; i < ids.length; i++) {
var row = that.container.find("#clients-tbl").getRowData(ids[i]);
var imgView = "<img src='../../Content/Images/vcard.png' style='cursor:pointer;' alt='Open case' onclick=OnClickImage(" + ids[i] + "); />";
that.container.find("#clients-tbl").setRowData(ids[i], { CasesButtons: imgView });
}
}
this.CreateClientsGrid = function () {
var clientsGrid = that.container.find("#widget-clients-tbl").jqGrid({
.....
ondblClickRow:function(rowid)
{
---
}
loadComplete: function () {
that.LoadClients();
}
}
this.OnClickImage=function(idClient){
....
}
this.Init = function () {
that.CreateClientsGrid();
};
this.Init();
}
The problem is with onclick, because OnClickImage is not global function.
How can I use OnClickImage function?
You can bind to the click event in different ways. For example you can follow the way from the answer. By the way, it works much more quickly as getRowData and setRowData. Moreover you should save the result of that.container.find("#clients-tbl") operation in a variable outside of the loop and use use the variable inside the loop. JavaScript is dynamic language and every operation even ids.length will be done every time.
One more way would to use onCellSelect event without click event binding. See the answer which describe the approach and gives the corresponding demo.

Categories