PDA

View Full Version : HTML event attrib vs DOM tree-traversal



a_design_interactive
01-11-2008, 10:06 PM
Hi there. Although I've been playing around w/ javaScript for at least a year now, i still consider myself to be new to JavaScript, and not all that good at it.

Somewhere along my studies in JavaScript, i've come to carry the notion that it's not considered to be in the best-practice to use intrinsic HTML event handler attributes, such as:
<p id="somePara" onclick="myFunc('somePara','someEl')">...</p>
in that it is preferred to use an external script to traverse the DOM Node tree, and set Event Listeners on the various DOM Element (http://developer.mozilla.org/en/docs/DOM:element#toc) Nodes, such as I've tried to do with the following script.

Note: the idea behind the script is to find each instance of an image with the HTML element class attribute of 'toggleGraphic', then to assign an event by determining the images position relative to a particular parent node, a DL element.
in the HTML, the image is a child of each DT, and the sibling DD is the element i want to "expand" or "collapse". I wouldn't expect the script to work, as-is, but i thought it would give the reader an idea of where i am in my understanding.


window.onload = getToggleGraphics;


function getToggleGraphics() {
var allImgs = document.getElementsByTagName ("IMG");
var allDds = document.getElementsByTagName("DD");
var imgClass = "toggleGraphic";
var ddClass = "collapsableDd";

for (var i=0; i<allImgs.length; i++) {

if (allImgs[i].className == imgClass) {
var thisDt = allImgs[i].parentNode;
var thisDl = thisDt.parentNode;
var allDlKids = thisDl.childNodes;

for(var j=0; j<allDlKids.length; j++){

if(allDlKids[j].nodeName == "DD"){
allImgs[i].onclick = toggleSection(allImgs[i],allDlKids[j]);
}
}
}
}

}

function toggleSection(thisImage, thisDd) {
var detailSection = thisDd;
var toggleImage = thisImage;

if (detailSection.style.display == 'block') {
detailSection.style.display = 'none';
toggleImage.src = 'css/collapsed.gif';
} else {
detailSection.style.display = 'block';
toggleImage.src = 'css/expanded.gif';
}

}

From the good neighbors and bright minds here at Dynamic Drive Forums, I'm looking for [any of the following]:
* a resource, with examples similar to my approach, where i might learn the right way
* deconstruct my code [above], citing where i've gotten off-track (in as little effort as possible for you!)
* share a suggested fix
* your own opinion of the event handler issue - intrinsic HTML attributes vs. DOM tree-traversal

thank you!

Trinithis
01-11-2008, 11:09 PM
The main problem with your code is that when you do


el.onclick = myFunc();

You are evaluating the value of myFunc, which in most cases is not a function. You want to do


el.onclick = myFunc;

Here, you are assigning a function to onclick. The problem is that you want to evaluate have the onclick call myFunc with some arguments passed in to do this, you need to wrap myFunc(x, y) in another function.

Try this. If it works, I'll explain it later if need be. (It consists of function literals and closures.)


function getToggleGraphics() {
var imgs = document.getElementsByTagName("img");
var imgClass = "toggleGraphic";
var dds;
for(var i = 0; i < imgs.length; ++i) {
if(imgs[i].className == imgClass) {
dds = imgs[i].parentNode.parentNode.getElementsByTagName("dd");
for(var j = 0; j < dds.length; ++j) {
imgs[i].onclick = (function(img, dd) {
return function() {
toggleSection(img, dd);
};
})(imgs[i], dds[j]);
}
}
}
}




your own opinion of the event handler issue - intrinsic HTML attributes vs. DOM tree-traversal

It depends to be honest. For your level of expertise, it would most likely be a matter of preference.

-----------

As a side note:

Currying (at least a form of it) would probably be the best option, but I have chosen not to use it for simplicity. Though I suppose understanding how to use a curry function isn't too bad. Understanding how to make one is another story. If you want, I can post this approach too.

One of our members, Twey has a tutorial on this site here: http://www.dynamicdrive.com/forums/showthread.php?t=23244
Another tutorial that is good is: http://ianhenderson.org/currying_in_javascript.html

a_design_interactive
01-12-2008, 02:23 AM
Trinithis,
thank you VERY much for your VERY useful reply. I appreciate that you took the time to explain my folly-- as this will help me a lot in my understanding.

I also appreciate that you provided some commentary on the very issue which has caused me to experiment with the DOM traversal approach. I'll definitely look into the resources you mentioned too.

will report back on the results of the code you wrote.

thank you!

PS. i'm curious what you mean by 'currying' (isn't that a Canadian sport of pushing stuff around on ice w/ a broom? hehe... naw!) i don't believe i've heard the term, but i'm always into learning the most long-term-useful ways of doing things. for example: sure, i would have been finished w/ this little project, had i used an HTML attribute for the event handler-- however, going the DOM traversal route, in theory-- i could add countless bits of the DL-DT-DD collapse thing into my html, and never need to modify the javascript-- and that's the direction i try to go with my code whenever possible (i.e. modular, scalable, etc.) -- do it right the first time, so to speak. thanks again.

a_design_interactive
01-12-2008, 03:47 AM
Trinithis, i've tried your code. at first, i was amazed-- thinking "wow! it works!", but it's not quite doing what i / we want. Although each of my "toggleGraphic" images does indeed 'expand and collapse' a DD (would we say 'successfully, the img has an onclick handler attached'?) as I wish, it is unfortunately a singular DD-- the last of the DL tree which is affected. the DL tree looks like:

<dl>
<dt><img class="toggleGraphic" /></dt>
<dd class="collapseMe">some content (01)</dd>
<dt><img class="toggleGraphic" /></dt>
<dd class="collapseMe">more content (02)</dd>
<dt><img class="toggleGraphic" /></dt>
<dd class="collapseMe">final content (03)</dd>
</dl> i didn't reference the w3c spec, but i think you can do a DL like that-- with multiple DT DD pairs as children. i don't know if it makes a difference for the code you wrote.

i have a question about your code. the section:

imgs[i].onclick = (function(img, dd) {
return function() {
toggleSection(img, dd);
};
})(imgs[i], dds[j]);you mention function literals and closures. i presume that is what we have here? i've worked w/ similar code-- but the part where
(imgs[i], dds[j]); is outside of the anonymous function-- or perhaps-- it's all part of a single expression-- as in
imgs[i].onclick = (anonymous.function)(imgs[i], dds[j]);... i dunno. i'm confused by that part-- but eager to learn. i'll have a look at the references you suggested in the meantime.

thanks!

EDIT: after reading the references on Currying in JavaScript, (including a third (http://www.svendtofte.com/code/curried_javascript/) referenced from ianhenderson.org) i'm not certain if [Trinithis is] using currying in the code provided for my problem-- but if it is, it appears to be some variation-- a bit unlike those in the examples at the references provided (above, by Trinithis), though similar enough to inspire curiosity. ;-)

definitely looking forward to a reply from Trinithis, hoping for some detail on the code i cited above.

EDIT2:
Using Firebug in Firefox to try to break down where the script is malfunctioning, so to speak, i decided to try logging calls to the first of the anonymous funcs-- the one w/ the args. the following is what Firebug illustrated:
(no name)(img.toggleGraphic collapsed.gif, dd.collapsableDd)
a closer look at (by hovering over the several) calls to this function reveals that it's not the image which has been counted-- as in img, img[2], img[3] etc, but DT, DT[2], DT[3]. so we've assigned a handler to an image which has no apparent differentiation-- but only by that DT which is it's parent.
So-- i'm thinking it is perhaps a good idea to re-work the script to operate off of the DT instead-- at least to some degree, maybe then targeting dt[0].firstChild, dt[1].firstChild, etc.
??

Trinithis
01-12-2008, 06:34 AM
Alright, I redid the code. Now I'm going to write up answers to your questions and post that when I'm done.



<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<title>Test</title>
<script type="text/javascript">

if(!Array.prototype.filter)
Array.prototype.filter = function(f /*, context */) {
for(var r = [], i = 0, n = this.length, t = arguments[1], v; i < n; ++i)
if(f.call(t, v = this[i], i, this))
r.push(v);
return r;
};

window.onload = function() {
var dl = document.getElementsByTagName("dl")[0];
var imgs = Array.prototype.filter.call(dl.getElementsByTagName("img"), function(el) {
return el.className = "toggleGraphic";
});
var dds = dl.getElementsByTagName("dd");
for(var i = imgs.length - 1; i >= 0; --i)
imgs[i].onclick = (function(i) {
return function() {
toggleSection(imgs[i], dds[i]);
};
})(i);
}

function toggleSection(img, dd) {
if(dd.style.display != "none") {
dd.style.display = "none";
img.src = "css/collapsed.gif";
}
else {
dd.style.display = "";
img.src = "css/expanded.gif";
}
}

</script>
</head>
<body><div>

<dl>
<dt><img class="toggleGraphic" alt="img" src="css/expanded.gif" /></dt>
<dd class="collapseMe">some content (01)</dd>
<dt><img class="toggleGraphic" alt="img" src="css/expanded.gif" /></dt>
<dd class="collapseMe">more content (02)</dd>
<dt><img class="toggleGraphic" alt="img" src="css/expanded.gif" /></dt>
<dd class="collapseMe">final content (03)</dd>
</dl>



</div>
</body>
</html>

Trinithis
01-12-2008, 07:43 AM
I'm just going to go through the basics first just in case, though you probably already know a little of this, if only by intuition.

------------------------------------

There are many names for function literals. Common ones include:

function literal
anonymous function
function expression
lambda
lambda expression



(The latter two are often used for a certain kind of function.)

A function literal is the following:



function(params) {code}


Notice there is no name for the function. In Javascript, functions are objects, meaning that you can assign them to variables.



function a() {alert("a");}
var b = a;
b();


Knowing this, you can define a brand new function by doing:



var add = function(x, y) {
return x + y;
};
alert(add(1, 2));


This is what we are doing with the el.onclick = function() {};

Seeing that you can assign functions to variables, it should be apparent that you can pass them to functions as well. After all, paramaters are variables.



alert([2,1,3,4,1,1].sort(function(a, b) {
if(a > b)
return 1;
if(a < b)
return -1;
return 0;
}));


What happens when you create a function inside a function? At first glance, it seems like nothing special. Note that the proper way of creating a function in any statement or function (when defining a function within a construct delimited by optional or mandatory curly brackets) is using the function expression syntax and not ordinary function syntax.



function a() {
var b = function() {
alert("b");
};
b();
}
a();
//b(); // uncommenting this statement gives an error


Alright, what about . . .



function a(x) {
var y = "y from a";
var b = function() {
alert(x);
alert(y);
};
b();
}
a("arg from a's x");


In the above example, the inner function knows all of a's local variables. Okay, now to the real question. What happens if the function a dies, but the function b lives? You might be wondering how this is possible, but observe:



function a(x) {
var y = "y from a";
return function() {
alert(x);
alert(y);
};
}

var val = a("arg from a's x");
val();


Clearly the function a has finished doing its work by the time val has a value assigned to it. Also, you have most likely heard about how the local variables in functions die out after the calling function ends from sources all over the place. Else your computer would soon run out of memory! Well, it turns out the Javascript (and in various other languages that support first-class functions . . . aka, where functions are treated like any other variable) has a built-in construct called a closure.

A closure is basically where local variables from all surrounding functions of a function remain in existence as long as the inner function lives. This is why the returned function stored in val knows about the variables x and y.



function a(x) {
return function() {
alert(x++);
};
}
var v1 = a(0);
var v2 = a(99);
var v3 = v1;
v1();
v1();
v2();
v2();
v3();


The above code also reveals to us that two different closures were created, each housing a different set of x's. As a matter of fact, even though v1 and v2 manage to change their x's, the change is only be seen in itself. (Remember though that v1 and v3 are the exact same function. In the case of changing x, the change would be seen between the two.)

Let's side track for a moment and go back to anonymous functions. There are situations where a function literal is never assigned to anything. In this case, it must execute itself. Here is the syntax:



(function(){})();


Examples:



(function() {
alert("self-executing function literal!");
})();

(function(x, y, z) {
alert(x + y + z);
})(1, 2, 3);


--------------------------------



imgs[i].onclick = (function(img, dd) {
return function() {
toggleSection(img, dd);
};
})(imgs[i], dds[j]);


Aha! The self-executing function literal! The point of this is to create a closure binding the img and dd values so the event handler function remembers them, so it in turn can use them for toggleSection.

If you are sharp, you might realize that those aren't the only variables bound by the closure. (BTW: Is it correct to use closure as a verb? Aka: Saying "closed" as in "bound by the closure"?) In that case, why not simply do:



imgs[i].onclick = return function() {
toggleSection(imgs[i], dds[j]);
};


The problem is that i and j change, and by the time the loop has completed, all the event handling functions you created will have the same i and j values. Oops! (In this case, they would also exceed the array bounds to boot.)

----------------------



i'm not certain if [Trinithis is] using currying in the code provided for my problem-- but if it is, it appears to be some variation-- a bit unlike those in the examples at the references provided (above, by Trinithis), though similar enough to inspire curiosity. ;-)


You aren't quite far off the track there :D. It isn't currying. Despite this, it is a form of partial application . . . kinda. (Currying is just a mechanism to implement partial application.)

Note: If any of the examples don't work, please let me know.

Trinithis
01-12-2008, 07:59 AM
i have a question about your code. the section:

imgs[i].onclick = (function(img, dd) {
return function() {
toggleSection(img, dd);
};
})(imgs[i], dds[j]);you mention function literals and closures. i presume that is what we have here? i've worked w/ similar code-- but the part where
(imgs[i], dds[j]); is outside of the anonymous function


I forgot to mention that there is another way to do the same exact thing:



imgs[i].onclick = (function() {
var img = imgs[i];
var dd = dds[j];
return function() {
toggleSection(img, dd);
};
})();

I tend to like paramater version of storing the variables more (the approach I used in the quote).

It's a matter of preference though; they are 100% identical in behavior and functionality.

a_design_interactive
01-12-2008, 08:01 AM
Hey there, Trinithis!

check it out! i was totally just coming back here to post this code, which-- after much much trial and error, and some genuine analysis of your (first rendition) solution, here's what i came up with-- which actually does work

function getToggleGraphics() {
var dts = document.getElementsByTagName("dt");
var dtClass = "sansSerifBold";
var dds;
var imgs;
for(var i = 0; i < dts.length; ++i) {
if(dts[i].className == dtClass) {
dds = dts[i].parentNode.getElementsByTagName("dd");
imgs = dts[i].parentNode.getElementsByTagName("img");

dts[i].onclick = (function(img, dd) {
return function() {
toggleSection(img, dd);
};
})(imgs[i], dds[i]);

}
}
}i wouldn't have been able to do, most importantly, without having your code to reference, and with a little help of Firebug. i got a lot out of your fingering the fact that i was using myFunc(); instead of myFunc; -- that i was evaluating, instead of assigning my desired Function to the onclick handler. I learned more of the function (or purpose) of the Anonymous Function, though i've seen and used it (basically by copy/ paste from other examples), it makes a LOT more sense to me now. ( <-- edit: especially after reading your subsequent posts.) also, i learned a lot about how Firebug works, and how might be used for debugging. [edit: i see, afterwards, that there is a proper manual (http://www.getfirebug.com/docs.html).]
(not to mention, i've been introduced to some entirely new concepts )

i look forward to any other commentary you might add to this thread†. again, i am grateful for your help, and fortunate to have caught your attention that you'd offer the input you've provided here. you're a good teacher. not everyone knows how to [i]teach, or explain things very well.
I wish i had more to offer in return-- if you haven't tried it, you might like the "IDE", Spket (http://developer.mozilla.org/en/docs/JavaScript#Tools), free for non-commercial use.

EDIT:Note - my reply was posted-- rather, began to post-- before the two replies, of the three subsequent (and very thoughtful; useful) posts by Trinithis in reply to my most-recent post.
† In other words-- from all you've given thus far-- i certainly don't mean to sound as if i expected more still! ;-)

Edit2: if you author your own web log / site, you should consider using the content/ concept of your tutorial here for a new article. IMHO, it's very well done, and would make a great resource for others. :-D

Trinithis
01-12-2008, 05:46 PM
Glad you found what I wrote useful. In any case, recently, I've decided to keep long posts like the one above in a text file in case I do make a tutorial.

Here's something that might be useful (or perhaps provoke more questions :p):
http://www.codingforums.com/showthread.php?t=129742

Key things to learn for better JS programming:

First-class functions
Function literals
Closures
JSON (JavaScript Object Notation)
Variable length arguments
The call() and apply() methods
The prototype object and property
Inheritance


My discussion above went over the first three, but only practice will allow you explore their uses.

For call and apply:
http://developer.mozilla.org/en/docs/Core_JavaScript_1.5_Reference:Objects:Function:apply
http://odetocode.com/Blogs/scott/archive/2007/07/04/11067.aspx

When I learned these for the first time, I was stuck in the mindset that they were used for inheritance purposes. (Because countless tutorials use them in that fashion.) That really cramped my style, and one I saw the light, it opens a whole load of doors. Note, I used the call method in post #5. See if you can figure it out after looking at the MDC documentation above :D.

Just skimming at some google results, this seems promising:
http://arstechnica.com/journals/linux.ars/2007/08/27/javascript-for-all-ages

Trinithis
01-14-2008, 02:34 AM
Meh, sorry about my code above. It has a memory leak. Here is it revised with added parts highlighted:



<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<title>Test</title>
<script type="text/javascript">

var addEvent = window.addEventListener
? function(el, t, f) {
el.addEventListener(t, f, false);
}
: function(el, t, f) {
el.attachEvent("on" + t, f);
};

if(!Array.prototype.filter)
Array.prototype.filter = function(f /*, context */) {
for(var r = [], i = 0, n = this.length, t = arguments[1], v; i < n; ++i)
if(f.call(t, v = this[i], i, this))
r.push(v);
return r;
};

window.onload = function() {
var dl = document.getElementsByTagName("dl")[0];
var imgs = Array.prototype.filter.call(dl.getElementsByTagName("img"), function(el) {
return el.className = "toggleGraphic";
});
var dds = dl.getElementsByTagName("dd");
for(var i = imgs.length - 1; i >= 0; --i)
imgs[i].onclick = (function(i) {
return function() {
toggleSection(imgs[i], dds[i]);
};
})(i);
addEvent(window, "unload", function() {
dl = imgs = dds = null;
});
}

function toggleSection(img, dd) {
if(dd.style.display != "none") {
dd.style.display = "none";
img.src = "css/collapsed.gif";
}
else {
dd.style.display = "";
img.src = "css/expanded.gif";
}
}

</script>
</head>
<body><div>

<dl>
<dt><img class="toggleGraphic" alt="img" src="css/expanded.gif" /></dt>
<dd class="collapseMe">some content (01)</dd>
<dt><img class="toggleGraphic" alt="img" src="css/expanded.gif" /></dt>
<dd class="collapseMe">more content (02)</dd>
<dt><img class="toggleGraphic" alt="img" src="css/expanded.gif" /></dt>
<dd class="collapseMe">final content (03)</dd>
</dl>



</div>
</body>
</html>


---------------------

@Twey or John (or others)

Would the following cause a leak?


imgs[i].onclick = toggleSection.bundle(null, imgs[i], dds[i]);


Given


Function.bundle = function(context, f, args) {
if(typeof f == "string" && context)
f = context[f];
if(arguments.length < 3)
args = [];
else if(!(args instanceof Array))
args = Array.prototype.slice.call(arguments, 2);
return function() {
return f.apply(context, args);
};
};

Function.prototype.bundle = function(context, args) {
return Function.bundle(context, this, args);
};