I'm struggling with managing dynamically built event handlers in javascript.
In several places, I build forms, or controls in which specific events (mainly mouseovers, mouse-outs, clicks) need to be handled.
The trick is that in a significant number of cases, the event handler itself needs to incorporate data that is either generated by, or is passed-into the function that is building the form or control.
As such, I've been using "eval()" to construct the events and incorporate the appropriate data, and this has worked somewhat well.
The problem is I keep seeing/hearing things like "You should never use eval()!" as well as a couple of increasingly ugly implementations where my dynamically-built event handler needs to dynamically build other event handlers and the nested evals are pretty obtuse (to put it mildly).
So I'm here, asking if someone can please show me the better way (native javascript only please, I'm not implementing any third-party libraries!).
Here's a crude example to illustrate what I'm talking about:
function CreateInput(controlName,type,activeStyle,dormantStyle,whenClicked)
{
var inp = document.createElement('input');
inp.id = controlName;
inp.type = type;
inp.style.cssText = dormantStyle;
eval("inp.onfocus = function() { this.style.cssText = '" + activeStyle + "'; }");
eval("inp.onblur = function() { this.style.cssText = '" + dormantStyle + "'; }");
eval("inp.onclick = function() { " + whenClicked + "; }");
return inp;
}
This function obviously would let me easily create lots of different INPUT tags and specify a number of unique attributes and event actions, with just a single function call for each. Again, this is an extremely simplified example, just to demonstrate what I'm talking about, in some cases with the project I'm on currently, the events can incorporate dozens of lines, they might even make dynamic ajax calls based on a passed parameter or other dynamically generated data. In more extreme cases I construct tables, whose individual rows/columns/cells may need to process events based on the dynamically generated contents of the handler, or the handler's handler.
Initially, I had built functions like the above as so:
function CreateInput(controlName,type,activeStyle,dormantStyle,whenClicked)
{
var inp = document.createElement('input');
inp.id = controlName;
inp.type = type;
inp.style.cssText = dormantStyle;
inp.onfocus = function() { this.style.cssText = activeStyle; };
inp.onblur = function() { this.style.cssText = dormantStyle; };
eval("inp.onclick = function() { " + whenClicked + "; }");
return inp;
}
...but I found that whatever the last assigned value had been for "activeStyle", and "dormantStyle" became the value used by all of the handlers thusly created (instead of each retaining its own unique set of styles, for example). That is what lead me to using eval() to "lock-in" the values of the variables when the function was created, but this has lead me into nightmares such as the following:
(This is a sample of one dynamically-built event-handler that I'm currently working on and which uses a nested eval() function):
eval("input.onkeyup = function() { " +
"InputParse(this,'ucwords'); " +
"var tId = '" + myName + This.nodeName + "SearchTable" + uidNo + "'; " +
"var table = document.getElementById(tId); " +
"if (this.value.length>2) { " +
"var val = (this.value.indexOf(',') >=0 ) ? this.value.substr(0,this.value.indexOf(',')) : this.value; " +
"var search = Global.LoadData('?fn=citySearch&limit=3&value=' + encodeURI(val)); " +
"if (table) { " +
"while (table.rows.length>0) { table.deleteRow(0); } " +
"table.style.display='block'; " +
"} else { " +
"table = document.createElement('table'); " +
"table.id = tId; " +
"ApplyStyleString('" + baseStyle + ";position=absolute;top=20px;left=0px;display=block;border=1px solid black;backgroundColor=rgba(224,224,224,0.90);zIndex=1000;',table); " +
"var div = document.getElementById('" + divName + "'); " +
"if (div) { div.appendChild(table); } " +
"} " +
"if (search.rowCount()>0) { " +
"for (var i=0; i<search.rowCount(); i++) { " +
"var tr = document.createElement('tr'); " +
"tr.id = 'SearchRow' + i + '" + uidNo + "'; " +
"tr.onmouseover = function() { ApplyStyleString('cursor=pointer;color=yellow;backgroundColor=rgba(40,40,40,0.90);',this); }; " +
"tr.onmouseout = function() { ApplyStyleString('cursor=default;color=black;backgroundColor=rgba(224,224,224,0.90);',this); }; " +
"eval(\"tr.onclick = function() { " +
"function set(id,value) { " +
"var o = document.getElementById(id); " +
"if (o && o.value) { o.value = value; } else { alert('Could not find ' + id); } " +
"} " +
"set('" + myName + This.nodeName + "CityId" + uidNo + "','\" + search.id(i)+ \"'); " +
"set('" + myName + This.nodeName + "ProvId" + uidNo + "','\" + search.provId(i)+ \"'); " +
"set('" + myName + This.nodeName + "CountryId" + uidNo + "','\" + search.countryId(i) + \"'); " +
"set('" + input.id + "','\" + search.name(i)+ \"'); " +
"}\"); " +
"var td = document.createElement('td'); " +
"var re = new RegExp('('+val+')', 'gi'); " +
"td.innerHTML = search.name(i).replace(re,'<span style=\"font-weight:bold;\">$1</span>') + ', ' + search.provinceName(i) + ', ' + search.countryName(i); " +
"tr.appendChild(td); " +
"table.appendChild(tr); " +
"} " +
"} else { " +
"var tr = document.createElement('tr'); " +
"var td = document.createElement('td'); " +
"td.innerHTML = 'No matches found...';" +
"tr.appendChild(td); " +
"table.appendChild(tr); " +
"} " +
"} else { " +
"if (table) table.style.display = 'none'; " +
"} " +
"} ");
Currently, I'm having problems getting the nested eval() to bind the ".onclick" event to the table-row, and, as you can see, figuring out the code is getting pretty hairy (debugging too, for all the known reasons)... So, I'd really appreciate it if someone could point me in the direction of being able to accomplish these same goals while avoiding the dreaded use of the "eval()" statement!
Thanks!
And this, among many other reasons, is why you should never use eval. (What if those values you're "baking" in contain quotes? Oops.) And more generally, try to figure out why the right way doesn't work instead of beating the wrong way into submission. :)
Also, it's not a good idea to assign to on* attributes; they don't scale particularly well. The new hotness is to use element.addEventListener, which allows multiple handlers for the same event. (For older IE, you need attachEvent. This kind of IE nonsense is the primary reason we started using libraries like jQuery in the first place.)
The code you pasted, which uses closures, should work just fine. The part you didn't include is that you must have been doing this in a loop.
JavaScript variables are function-scoped, not block-scoped, so when you do this:
var callbacks = [];
for (var i = 0; i < 10; i++) {
callbacks.push(function() { alert(i) });
}
for (var index in callbacks) {
callbacks[index]();
}
...you'll get 9 ten times. Each run of the loop creates a function that closes over the same variable i, and then on the next iteration, the value of i changes.
What you want is a factory function: either inline or independently.
for (var i = 0; i < 10; i++) {
(function(i) {
callbacks.push(function() { alert(i) });
})(i);
}
This creates a separate function and executes it immediately. The i inside the function is a different variable each time (because it's scoped to the function), so this effectively captures the value of the outer i and ignores any further changes to it.
You can break this out explicitly:
function make_function(i) {
return function() { alert(i) };
}
// ...
for (var i = 0; i < 10; i++) {
callbacks.push(make_function(i));
}
Exactly the same thing, but with the function defined independently rather than inline.
This has come up before, but it's a little tricky to spot what's causing the surprise.
Even your "right way" code still uses strings for the contents of functions or styles. I would pass that click behavior as a function, and I would use classes instead of embedding chunks of CSS in my JavaScript. (I doubt I'd add an ID to every single input, either.)
So I'd write something like this:
function create_input(id, type, active_class, onclick) {
var inp = document.createElement('input');
inp.id = id;
inp.type = type;
inp.addEventListener('focus', function() {
this.className = active_class;
});
inp.addEventListener('blur', function() {
this.className = '';
});
inp.addEventListener('click', onclick);
return inp;
}
// Called as:
var textbox = create_input('unique-id', 'text', 'focused', function() { alert("hi!") });
This has some problems still: it doesn't work in older IE, and it will remove any class names you try to add later. Which is why jQuery is popular:
function create_input(id, type, active_class, onclick) {
var inp = $('<input>', { id: id, type: type });
inp.on('focus', function() {
$(this).addClass(active_class);
});
inp.on('blur', function() {
$(this).removeClass(active_class);
});
inp.on('click', onclick);
return inp;
}
Of course, even most of this is unnecessary—you can just use the :focus CSS selector, and not bother with focus and blur events at all!
You don't need eval to "lock in" a value.
It's not clear from the posted code why you're seeing the values change after CreateInput returns. If CreateInput implemented a loop, then I would expect the last values assigned to activeStyle and dormantStyle to be used. But even calling CreateInput from a loop will not cause the misbehavior you describe, contrary to the commenter.
Anyway, the solution to this kind of stale data is to use a closure. JavaScript local variables are all bound to the function call scope, no matter if they're declared deep inside the function or in a loop. So you add a function call to force new variables to be created.
function CreateInput(controlName,type,activeStyle,dormantStyle,whenClicked)
{
while ( something ) {
activeStyle += "blah"; // modify local vars
function ( activeStyle, dormantStyle ) { // make copies of local vars
var inp = document.createElement('input');
inp.id = controlName;
inp.type = type;
inp.style.cssText = dormantStyle;
inp.onfocus = function() { this.style.cssText = activeStyle; };
inp.onblur = function() { this.style.cssText = dormantStyle; };
inp.onclick = whenClicked;
}( activeStyle, dormantStyle ); // specify values for copies
}
return inp;
}
Related
Okay, that title will sound a bit crazy. I have an object, which I build from a bunch of inputs (from the user). I set them according to their value received, but sometimes they are not set at all, which makes them null. What I really want to do, it make an item generator for WoW. The items can have multiple attributes, which all look the same to the user. Here is my example:
+3 Agility
+5 Stamina
+10 Dodge
In theory, that should just grab my object's property name and key value, then output it in the same fashion. However, how do I setup that if-statement?
Here is what my current if-statement MADNESS looks like:
if(property == "agility") {
text = "+" + text + " Agility";
}
if(property == "stamina") {
text = "+" + text + " Stamina";
}
if(property == "dodge") {
text = "+" + text + " Dodge";
}
You get that point right? In WoW there are A TON of attributes, so it would suck that I would have to create an if-statement for each, because there are simply too many. It's basically repeating itself, but still using the property name all the way. Here is what my JSFiddle looks like: http://jsfiddle.net/pm2328hx/ so you can play with it yourself. Thanks!
EDIT: Oh by the way, what I want to do is something like this:
if(property == "agility" || property == "stamina" || ....) {
text = "+" + text + " " + THE_ABOVE_VARIABLE_WHICH_IS_TRUE;
}
Which is hacky as well. I definitely don't want that.
if(['agility','stamina','dodge'].indexOf(property) !== -1){
text = "+" + text + " " + property;
}
If you need the first letter capitalized :
if(['agility','stamina','dodge'].indexOf(property) !== -1){
text = "+" + text + " " + property.charAt(0).toUpperCase() + property.substr(1);
}
UPDATE per comment:
If you already have an array of all the attributes somewhere, use that instead
var myatts = [
'agility',
'stamina',
'dodge'
];
if(myatts.indexOf(property) !== -1){
text = "+" + text + " " + property.charAt(0).toUpperCase() + property.substr(1);
}
UPDATE per next comment:
If you already have an object with the attributes as keys, you can use Object.keys(), but be sure to also employ hasOwnProperty
var item = {};
item.attribute = {
agility:100,
stamina:200,
dodge:300
};
var property = "agility";
var text = "";
if(Object.keys(item.attribute).indexOf(property) !== -1){
if(item.attribute.hasOwnProperty(property)){
text = "+" + text + " " + property.charAt(0).toUpperCase() + property.substr(1);
}
}
Fiddle: http://jsfiddle.net/trex005/rk9j10bx/
UPDATE to answer intended question instead of asked question
How do I expand the following object into following string? Note: the attributes are dynamic.
Object:
var item = {};
item.attribute = {
agility:100,
stamina:200,
dodge:300
};
String:
+ 100 Agility + 200 Stamina + 300 Dodge
Answer:
var text = "";
for(var property in item.attribute){
if(item.attribute.hasOwnProperty(property)){
if(text.length > 0) text += " ";
text += "+ " + item.attribute[property] + " " + property.charAt(0).toUpperCase() + property.substr(1);
}
}
It's unclear how you're getting these values an storing them internally - but assuming you store them in a hash table:
properties = { stamina: 10,
agility: 45,
...
}
Then you could display it something like this:
var text = '';
for (var key in properties) {
// use hasOwnProperty to filter out keys from the Object.prototype
if (h.hasOwnProperty(k)) {
text = text + ' ' h[k] + ' ' + k + '<br/>';
}
}
After chat, code came out as follows:
var item = {};
item.name = "Thunderfury";
item.rarity = "legendary";
item.itemLevel = 80;
item.equip = "Binds when picked up";
item.unique = "Unique";
item.itemType = "Sword";
item.speed = 1.90;
item.slot = "One-handed";
item.damage = "36 - 68";
item.dps = 27.59;
item.attributes = {
agility:100,
stamina:200,
dodge:300
};
item.durability = 130;
item.chanceOnHit = "Blasts your enemy with lightning, dealing 209 Nature damage and then jumping to additional nearby enemies. Each jump reduces that victim's Nature resistance by 17. Affects 5 targets. Your primary target is also consumed by a cyclone, slowing its attack speed by 20% for 12 sec.";
item.levelRequirement = 60;
function build() {
box = $('<div id="box">'); //builds in memory
for (var key in item) {
if (item.hasOwnProperty(key)) {
if (key === 'attributes') {
for (var k in item.attributes) {
if (item.attributes.hasOwnProperty(k)) {
box.append('<span class="' + k + '">+' + item.attributes[k] + ' ' + k + '</span>');
}
}
} else {
box.append('<span id="' + key + '" class="' + item[key] + '">' + item[key] + '</span>');
}
}
}
$("#box").replaceWith(box);
}
build();
http://jsfiddle.net/gp0qfwfr/5/
I have a few JavaScript functions designed to add and remove HTML divs to a larger div. The function init is the body's onload. New lines are added when an outside button calls NewLine(). Divs are removed when buttons inside said divs call DeleteLine(). There are a few problems with the code though: when I add a new line, the color values of all the other lines are cleared, and when deleting lines, the ids of the buttons, titles, and line boxes go out of sync. I've gone through it with the Chrome debugger a few times, but each time I fix something it seems to cause a new problem. I would greatly appreciate some input on what I'm doing wrong.
function init()
{
numOfLines = 0; //Keeps track of the number of lines the Artulator is displaying
}
function NewLine()
{
var LineBoxHolder = document.getElementById("LineBoxHolder");
numOfLines += 1;
LineBoxCode += "<div class = 'Line Box' id = 'LineBox" + numOfLines + "'>" //The code is only split onto multiple lines to look better
+ " <h6 id = 'Title " + numOfLines + "' class = 'Line Box Title'>Line " + numOfLines + "</h6>";
+ " <p>Color: <input type = 'color' value = '#000000'></p>"
+ " <input type = 'button' value = 'Delete Line' id = 'DeleteLine" + numOfLines + "' onclick = 'DeleteLine(" + numOfLines + ")'/>"
+ "</div>";
LineBoxHolder.innerHTML += LineBoxCode;
}
function DeleteLine(num)
{
deletedLineName = "LineBox" + num;
deletedLine = document.getElementById(deletedLineName);
deletedLine.parentNode.removeChild(deletedLine);
num++;
for ( ; num < numOfLines + 1 ; )
{
num++;
var newNum = num - 1;
var changedLineName = "LineBox" + num;
var changedHeaderName = "Title" + num;
var changedButtonName = "DeleteLine" + num;
var changedButtonOC = "DeleteLine(" + newNum + ")";
var changedLine = document.getElementById(changedLineName);
var changedHeader = document.getElementById(changedHeaderName);
var changedButton = document.getElementById(changedButtonName);
var changedLine.id = "LineBox" + newNum;
var changedHeader.innerHTML = "Line" + newNum;
var changedHeader.id = "Title" + newNum;
var changedButton.setAttribute("onclick",changedButtonOC);
var changedButton.id = "DeleteLine" + newNum;
}
num--;
numOfLines = num;
}
You are having a hard time debugging your code because of your approach. You are "marking" various elements with the IDs you construct, and using the IDs to find and address elements. That means that when things change, such as line being deleted, you have to go back and fix up the markings. Almost by definition, the complicated code you wrote to do something like that is going to have bugs. Even if you had great debugging skills, you'd spend some time working through those bugs.
Do not over-use IDs as a poor-man's way to identify DOM elements. Doing it that way requires constructing the ID when you create the element and constructing more IDs for the sub-elements. Then to find the element again, you have to construct another ID string and do getElementById. Instead, use JavaScript to manage the DOM. Instead of passing around IDs and parts of IDs like numbers, pass around the DOM elements themselves. In your case, you don't need IDs at all.
Let's start off with DeleteLine. Instead of passing it a number, pass it the element itself, which you can do my fixing the code inside your big DOM string to be as follows:
<input type='button' value='Delete Line' onclick="DeleteLine(this.parentNode)"/>
So we have no ID for the line element, no ID for the element, and no ID within the onclick handler. DeleteLine itself can now simply be
function DeleteLine(line) {
{
line.parentNode.removeChild(line);
renumberLines();
}
We'll show renumberLines later. There is no need to adjust IDs, rewrite existing elements, or anything else.
Since we no longer need the ID on each line or its sub-elements, the code to create each element becomes much simpler:
function NewLine()
{
var LineBoxHolder = document.getElementById("LineBoxHolder");
numOfLines += 1;
var LineBoxCode = "<div class='LineBox'>" +
+ " <h6 class='LineBoxTitle'>Line " + "numOfLines + "</h6>"
+ " <p>Color: <input type='color' value='#000000'></p>"
+ " <input type='button' value='Delete Line' onclick= 'DeleteLine(this.parentNode)'/>"
+ "</div>";
LineBoxHolder.innerHTML += LineBoxCode;
}
The only remaining work is to fix up the titles to show the correct numbers. You can do this by just looping through the lines, as in
function renumberLines() {
var LineBoxHolder = document.getElementById("LineBoxHolder");
var lines = LineBoxHolder.childElements;
for (var i = 0; i < lines.length; i++) {
var line = lines[i];
var h6 = line.querySelector('h6');
h6.textContent= "Line " + (i+1);
}
}
I voted to close because the question is too broad, but will answer anyway on a few points to... well, point in the right direction.
var changedButton.setAttribute("onclick",changedButtonOC); This is not a variable declaration. Omit the var.
for ( ; num < numOfLines + 1 ; ) { num++; ... The correct form here would be simply for (; num < numOfLines + 1; num++) { ....
Instead of incrementing (num++) then decrementing (num--) around the loop, why not just use the right math?
See:
for (; num < numOfLines; num++) {
...
}
I was wondering how can I make it posible to get rid of putting "new" before a function, for example:
new functionToDo("thingsToDo").iGotYouBruh("Halo Humans");
is there a posible way of doing this without the "new"?
here is the code I'm trying to use without the "new":
function local (title) {
var storeTitle = title;
this.addL = function(lString) {
var storeText = lString;
localStorage.setItem(storeTitle, storeText);
console.info("Locally stored " + storeTitle.toUpperCase() + " with " + storeText.substring(0, 10) + "... As text.");
};
this.removeL = function() {
localStorage.removeItem(storeTitle);
console.info("Locally removed " + storeTitle + ".");
};
this.getL = function () {
localStorage.getItem(storeTitle);
console.info("Locally got string of " + storeTitle + ": " + localStorage.getItem(storeTitle));
};
};
and here's what I would have to do to invoke the function:
new local("storedElement").getL();
This is possible by checking whether this is an instance of the function itself and returning a new instance otherwise:
function local (title) {
if (!(this instanceof local)) {
return new local(title);
}
var storeTitle = title;
this.addL = function(lString) {
var storeText = lString;
localStorage.setItem(storeTitle, storeText);
console.info("Locally stored " + storeTitle.toUpperCase() + " with " + storeText.substring(0, 10) + "... As text.");
};
this.removeL = function() {
localStorage.removeItem(storeTitle);
console.info("Locally removed " + storeTitle + ".");
};
this.getL = function () {
localStorage.getItem(storeTitle);
console.info("Locally got string of " + storeTitle + ": " + localStorage.getItem(storeTitle));
};
};
You could use JavaScript closures. In particular look at the "Using Closures for the Module Pattern" section of this webpage for a full description. The idea is to have the function return an literal with all the required methods. Any functions or variables that you want to be kept private are just local variables for the function.
Does anyone know why this isn't passing?
function correctColorDisplay(message, player_turn, selector) {
if ((message > 0) && (player_turn != 0)) {
return $(selector).append("<li>" + message + " " + "color(s) are present but not in the correct position in Round " + player_turn + ".</li>");
}
}
Jasmine:
describe('#correctColorDisplay', function(){
it('returns a message to the user displaying if a correct color (not positions) was chosen', function(){
var message = 2
var playerTurn = 2
var selector = $('<li></li>')
correctColorDisplay(message,playerTurn, selector)
expect(selector).toMatch("<li>" + message + " " + "color(s) are present but not in the correct position in Round " + playerTurn + ".</li>")
});
});
The error I keep getting is this giant message: Expected { 0 : HTMLNode, length : 1, jquery : '1.11.0', constructor : Function, selector : '', toArray : Function, get : Function, pushStack : Function, each, etc (it goes on much longer)
You are trying to match a newly created HTMLNode with a regular expression (that is basically just a string in this case).
The toMatch function of Jasmine is for regular expressions.
I'm not entirely familiar with Jasmine, but I'm guessing you're looking for something like:
describe('#correctColorDisplay', function(){
it('returns a message to the user displaying if a correct color (not positions) was chosen', function() {
var message = 2;
var playerTurn = 2;
var selector = $('<li></li>');
selector = correctColorDisplay(message, playerTurn, selector);
expect(selector).toEqual( $("<li><li>" + message + " " + "color(s) are present but not in the correct position in Round " + playerTurn + ".</li></li>") );
});
});
If that doesn't work, I suggest you look into jasmine-jquery.
This could be an easy task but I am just learning the relationship between jQuery, JSON, and Javascript. I used jQuery to to pull from my database and create a variable called res[i].showlink which is a url. Here is part of my code for the call.
$.get("http://databasecall=json", {}, function (res) {
$.mobile.hidePageLoadingMsg();
if (res.length) {
var s = "";
for (var i = 0; i < res.length; i++) {
s += "<li><a name=" + res[i].id + " href='" + "javascript:openGoogle()" + "'>" + res[i].showlink + "</a></li>";
}
$("#showList").html(s);
$("#showList").listview("refresh")
}, "json");
The problem is that I would like reuse the res[i].showlink database variable in a javascript function (openGoogle) outside of the code above. When I go to reuse the database variable res[i].showlink, it no longer contains my data from the database. How can I reuse the variable outside of the jQuery/JSON code above? I really appreciate any suggestions. Thank you!
You are storing that value as the contents of the anchor tag, you can access it from there too.
change
javascript:openGoogle()
to
javascript:openGoogle.apply(this)
and then inside of openGoogle, you can access the value with $(this).text()
Edit
Another option is to pass the value directly as a parameter.
change
javascript:openGoogle()
to
javascript:openGoogle(" + res[i].showlink + )
and then modify
function openGoogle() {
to
function openGoogle(showlink) {
and access the value with
alert(showlink);
You need to store a reference to res outside of the get call--otherwise, it's scoped and, as you noticed, you can't access it from outside the call. Try something like this:
var globalRes = null;
$.get("http://databasecall=json", {}, function(res) {
globalRes = res;
$.mobile.hidePageLoadingMsg();
if (res.length) {
var s = "";
for (var i = 0; i < res.length; i++) {
s += "<li><a name=" + res[i].id + " href='" + "javascript:openGoogle()" + "'>" + res[i].showlink + "</a></li>";
}
$("#showList").html(s);
$("#showList").listview("refresh")
}, "json");
After the call, globalRes will contain the value of res, but will be global, meaning you can access it from outside the get call.
save the value in the global variable:
$.get("http://databasecall=json", {}, function (res) {
window.myResult = res;
....
}
and use it afterwards
You need to store it in a variable that has scope outside of the ajax return call.
So for example you could have:
var resData;
at the top of an included javascript file
then replace your code with
$.get("http://databasecall=json", {}, function(res) {
$.mobile.hidePageLoadingMsg();
if(res.length){
resData = res;
var s = "";
for(var i=0; i<res.length; i++) {
s+= "<li><a name=" + res[i].id + " href='" + "javascript:openGoogle()" + "'>" + res[i].showlink + "</a></li>";
}