How to Associate an Object with a DOM Element - javascript

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.

Related

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

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>

Passing object variable to onchange event JS

I've been browsing around for an answer to this question for a little while now, but haven't found a solution. I need to pass an object to a function which is being fired "onChange" when a select option is chosen by a user. The current code is:
selecter.onchange = function(){
var runScript = $("#actionSel option:selected").attr('script');
console.log(runScript);
eval("("+runScript+")();");
}
The intention here is to store a function within the "script" attr of the options, which will then be run when that option is selected. However, for one of my functions I need to pass a variable from the parent scope on in order to interact with the server via websockets.
The function stored in the "Script" attribute is :
function(){ popConfirm("Restore User","Do you really want to restore the selected users? This will un-delete the selected deleted users.",function(r){ if(r)restoreUser(r,io); });
Essentially this verifies with the user that they want to do what they've selected, then passes the result to my restore user function. It also needs to pass the "io" object on. However, I'm getting an error which states that io is undefined.
Any ideas would be extremely helpful. Thanks!
As requested, here are some additional relative pieces of code showing where IO is introduced.
AdminIO = new io(servPath);
AdminIO.on('send_users',function(rows){
toggleLoad();
appendUsers(rows,AdminIO);
});
Within appendUsers there's another function which compiles the select list and its options, dropActions(), wherein the selector.onchange and other piece I posted before are introduced. The selector.onchange is part of the function that creates the dropdown list. The function(){ popConfirm() } is added as the function to run on selection of that item. The function to build the list is :
dropActions = function(bActions, lActions, options){
// bActions = {id: myID, text: "this is my action", elem: document.getElementById('getDiv'), action: function(){ /*mycode here */}}
// lActions = {text: "select me to run an action", action: function(){ /*mycode here */}}
bActions = bActions || null;
lActions = lActions || null;
options = options || {elem: document.body, id: null};
if(!bActions && !lActions){ console.error("No actions added or available."); return; }
var
selID = options.id,
starter = (selID) ? document.getElementById(selID) : options.elem,
optsBar = document.createElement("header"),
selecter = document.createElement("select");
starterSel = document.createElement("option");
optsBar.id = "actionSelH";
starterSel.innerText = "More Actions";
starterSel.setAttribute('script','javascript:void(0)');
selecter.id = "actionSel";
selecter.appendChild(starterSel);
for(var i= 0; bActions.length > i; i++){
var
buttonText = bActions[i].text,
buttonID = bActions[i].id || 'ACT'+i,
buttonAction = bActions[i].action,
button = document.createElement('div');
button.id = buttonID;
button.classList.add("actionButton");
button.innerText = buttonText;
button.onclick = buttonAction;
optsBar.appendChild(button);
}
for(var i= 0; lActions.length > i; i++){
var selText = lActions[i].text,
selScript = lActions[i].action,
option = document.createElement('option');
option.innerText = selText;
option.setAttribute('script',selScript);
selecter.appendChild(option);
}
selecter.onchange = function(){
var runScript = $("#actionSel option:selected").attr('script');
console.log(runScript);
eval("("+runScript+")();");
}
optsBar.appendChild(selecter);
$(optsBar).insertBefore('#user_list_table');
//$('#user_list_table').after(optsBar);
//$(starter).prepend(optsBar);
},
Hopefully more context helps!
Instead of using eval, you could use the function constructor and pass your parameters directly to that function:
var scriptFunction = new Function(r, io, runScript);
scriptFunction(r, io);
Of course, this code assume that r and io will always be the variable you are looking for. If this ain't the case, you'll have to write your own logic to take the variable from the parent scope and then pass them to the scriptFunction.
Update
If you are able ton change the provided script, you could also try to implicitly declare the variables r and io:
function(r, io){ popConfirm("Restore User","Do you really want to restore the selected users? This will un-delete the selected deleted users.",function(r, io){ if(r)restoreUser(r,io); });

Plain OOP Javascript: Treating localStorage as an Array doesn't work?

I am trying to implement localStorage with my simple OOP todo list.
The fiddle is here: https://jsfiddle.net/b81t2789/
I thought I could just treat the local storage like an array and copy the logic I used with my actual array but that doesn't work.
Here, right after pushing the task into the array, I added a line that stores the task in the local storage and stringifies it:
// function that adds new task to the array
function pushArray(){
var newtask = new Task(toDo.value, "No note yet");
taskItems.push(newtask);
var storedTask = localStorage.setItem(newtask, JSON.stringify(newtask));
displayStorage(result2, storedTask);
displayArray(result, newtask.Name);
appendNote(result, newtask);
}
Then right below the function that displays the new array element I added one that retrieves the item from local storage, parses it, then creates a DOM element with the new task and appends it to another container.
//function that displays array elements
function displayArray(parent,obj){
var task = make("div","class","taskitem",obj);
parent.appendChild(task);
fadeIn(task);
}
//function that displays storage elements
function displayStorage(parent,obj){
var retrieveObject = localStorage.getItem(obj);
var parseTask = JSON.parse(retrieveObject);
var newDiv = make("div", "class", "newdiv", parseTask);
parent.appendChild(newDiv);
fadeIn(newDiv);
}
This doesn't work at all, not sure why, and then if I were to be able to get this to work how would I continue to go about storing and updating notes like I did in the array with local Storage? I thought this would be easy as I figured out how to make a todo with objects and arrays pretty quickly (when I thought it would be super difficult, but it's been a week now and I've made no progress!)
I guess these are the pitfalls of learning to code by yourself, any help would be much appreciated thank you!
Here is the full javascript code:
//getElementById shortcut
function grab(id) {
return document.getElementById(id);
}
// add eventlistener shortcut
var when = function() {
return function(obj, event, func) {
obj.addEventListener(event, func, false);
};
}();
//Custom function to create DOM elements and set their contents
function make(el,type,name,content){
var theElement = document.createElement(el);
theElement.setAttribute(type, name);
theElement.innerHTML = content;
return theElement;
}
//compute style shortcut
function setStyle(theElement){
return window.getComputedStyle(theElement);
}
//fade in shortcut.
function fadeIn(theElement){
var compute = setStyle(theElement).opacity;
theElement.style.opacity = 1;
}
/*****************************************************/
var toDo = grab("todo");
var result = grab("demo");
var demolist = grab("demolist");
var button = grab("btn");
// submit input on enter which fires function that pushes task into the array.
when(toDo, "keypress", function(event){
if (event.key == "Enter" || event.keyCode == 13) {
pushArray();
toDo.value = "";
}
});
// "SHOW ARRAY" FUNCTION to verify that the array is being updated (I like this better than using the console);
when(button, "click", function(event){
demolist.innerHTML = "";
for(i=0; i< taskItems.length; i++){
demolist.innerHTML += taskItems[i].Name + " " + taskItems[i].Note + "<br>";
}
});
function showNotes(theNote){
var defaultNote = "No note yet";
if(theNote){
}
}
var taskItems = [];
/*********************************************************/
//create Task object
function Task(name, note){
this.Name = name;
this.Note = note;
this.completed = false;
}
// function that adds new task to the array
function pushArray(){
var newtask = new Task(toDo.value, "No note yet");
taskItems.push(newtask);
displayArray(result, newtask.Name);
appendNote(result, newtask);
}
//function that displays array elements
function displayArray(parent,obj){
var task = make("div","class","taskitem",obj);
parent.appendChild(task);
fadeIn(task);
}
//function that displays notes
function appendNote(theElement,obj){
var newClassItem = make("input","class","tasknote");
theElement.appendChild(newClassItem);
when(newClassItem, "keypress", submitNote.bind(null, obj, newClassItem));
}
//function for submitting notes
function submitNote(task,noteInput){
if (event.key == "Enter" || event.keyCode == 13) {
task.Note = noteInput.value;
var newNote = make("div", "class", "hasNote", task.Note);
noteInput.parentNode.replaceChild(newNote, noteInput);
fadeIn(newNote);
when(newNote,"dblclick", function(){
newNote.parentNode.replaceChild(noteInput, newNote);
});
}
}
Being localStorage a key-value storage, depending on your needs, you are better off serializing (stringifying, whatever) the array and saving in a single index.
var tasks = [
'post the question on SO',
'describe it carefully',
'get a nice reply',
'implement the suggested solution'
];
If you really need to split it for performance reasons, you have to index them by a arbitrary index. If you have reordering it gets tricky and you can either reflush the whole set of tasks every time someone adds/edits/deletes/reorder the tasks (memory-efficient, but very CPU intensive) or save the indexes in a different key so you can reconstruct the order later, like:
var tasks = {
'task1': 'implement the suggested solution',
'task2': 'describe it carefully',
'task4': 'get a nice reply',
'task9': 'post the question on SO'
};
var tasksOrder = [9, 2, 4, 1];
The first idea is very simple to implement, but will give you problems with arbitrarily long lists, the second one is much more easy on the CPU but much harder to implement (and uses more memory). It depends on the specifics of your case.

Obtain Javascript object from the dom element in google closure

Is there a way to obtain the underlying object from dom element.
For instance.
I have a base class which renders some buttons using
goog.ui.CustomButton.
In the child class I want to get underlying goog.ui.CustomButton
object through the generated dom element.
Is it possible or do I need to cache the underlying objects:
mylib.Base = function() {
};
mylib.Base.prototype.renderButtons = function(buttonTypes) {
for(var i in buttonTypes) {
var button = new goog.ui.CustomButton();
var buttonContainer = goog.dom.getElement('button-container_' + buttonTypes[i]);
button.render(buttonContainer);
}
};
mylib.Child = function() {
mylib.Base.base(this, 'constructor');
};
goog.inherits(mylib.Child, mylib.Base);
mylib.Super.prototype.someTask = function() {
this.renderButtons(['save_button','cancel_button']);
var saveButtonHolder = goog.dom.getElement('button-container_' + 'save_button');
var saveButtonElement = saveButtonHolder.childern[0]; // Not production code
// Now Is it possible to get the goog.ui.CustomButton object through the
// saveButtonElement ?
};

How to connect between prototype chain of objects and HTML representing them?

I have a card game, and cards are represented by Javascript objects that are created as instances of class (card > card-type > card-instance). I did it like this so that the cards can share methods.
Then I construct the HTML, and the cards suppose to be able to do all kinds of stuff, like move or attack for example.
move is defined in Card.prototype.move = function... and attack is UnitCard.prototype.attack
and now I am trying to connect the Card objects to their corresponding HTML elements, so that I will be able to so something like
$('#board').on('click', '.card', function (e) {
this.move(this.location, newLocation);
});
An idea I had is to make all the data and functions of the cards part of the DOM, and insert an object somewhere along the prototype chain of the DOM elements, so that the HTML of that card will have a move function. I know this idea is a bit crazy, but I am trying to avoid constant lookups inside objects (find the clicked card by name in the array of all cards and then if other cards that have influence on the action find them in the dom and then find them in the object etc...)
Any suggestions on how to solve this issue?
UPDATE - Current Code:
var Card = function (type, name, epoch) {
var cardHtml = document.createElement('div');
cardHtml.className += "card";
cardHtml.id = name;
cardHtml.cardType = type;
cardHtml.cardName = name;
cardHtml.cardEpoch = epoch;
this.cardHtml = cardHtml;
}
var Agent = function (cardProps, subtype, description, strike, shield, price) {
//fixed
Card.apply(this, cardProps);
this.subtype = subtype;
this.price = price; //agenda
//changable
this.cardHtml.innerHTML = ss.tmpl['cards-agent'].render({
name: this.name,
});
this.cardHtml.strike = strike;
this.cardHtml.shield = shield;
this.cardHtml.location = []; //board/hand/deck/grveyard
}
Agent.prototype = Object.create(Card.prototype);
Agent.prototype.move = function (currentLocation, newLocarion) {
console.log('move');
}
Store a reference to the instance on the element's data object.
var Card = function (type, name, epoch) {
var cardHtml = document.createElement('div');
cardHtml.className += "card";
cardHtml.id = name;
cardHtml.cardType = type;
cardHtml.cardName = name;
cardHtml.cardEpoch = epoch;
this.cardHtml = cardHtml;
$(cardHtml).data("card",this);
}
Now you can access it within events as needed.
$('#board').on('click', '.card', function (e) {
var card = $(this).data('card');
card.move(card.location, newLocation);
});
This of course assumes you can use jquery, per the jquery you're using in your question.
I can think of two additional options.
You could use bind to create a click handler in which this is actually your object instead of the dom element.
el.onclick = (function(){/* ... */}).bind(yourObj);
In this case, within your function, this would be your object instead of the dom element. As long as your object stores a reference to the dom element itelf, then you're set.
Another option would be to define the click handler within a closure which has a variable containing the object.
function bindHanlder(yourObj, el){
el.onclick = function(){
// "yourObj" can be used here.
};
}
I assume that your board has specific places where your cards can be placed.
You should have a Board object containing an array of Places, something like:
function Card(divId){
this.divId = divId;
//generate Card html elements with jquery
}
function Place(divId){
var currentCard = null;
//attach this javascript object to an html element
this.divId = divId;
//register events of this Place where this Place has this.divId
/*
$(document).on("click", this.divId, function(){
});
*/
this.setCard = function(card){
currentCard = card;
//use jquery to add Card html elements to this Place html element
}
}
function Board(){
var places= new Array();
this.addPlace = function(place){
places.push(place);
}
this.moveCard = function(card, toPlace){
toPlace.setCard(card);
}
}
var card1 = new Card("#divCard1");
var card2 = new Card("#divCard2");
var place1 = new Place("#divPlace1");
var place2 = new Place("#divPlace2");
var board = new Board();
board.addPlace(place1);
board.addPlace(place2);
board.moveCards(card1,place1);
This is really off the top of my head. I don't even know if it runs or not. It's just to give you an idea. Interpret it as pseudo code.
Good luck!

Categories