Monday, July 28, 2008

$ And Scoping

There are a number of javascript toolkits that are gaining some traction right now. For the most part, they're great. They abstract away the differences between browsers and provide a ton of really useful library/utility functions.

One such toolkit is Prototype JS. I've had the chance to play with it a bit and although I still prefer JQuery, it's not bad. There is, however, one pattern that I've noticed emerging in prototype-based web apps that I find a bit disturbing. I'm not sure if it's the fault of prototype or of developers that are abusing a prototype feature but it's certainly dangerous.

It's the handy $( ) utility function. It takes an id of an object in the DOM and returns that object with some prototype extensions monkey-patched on. Now this is not intended to be a document.getElementByID() wrapper as far as I can tell. It's also capable of taking an element and returning a prototype-extended version of it. So really, it's meant to be a quick method for prototype-monkey-patching a DOM element. Despite this, I'm afraid that it's higher purpose is generally ignored, and it's more frequently used as a succinct element fetcher.

The Prototype documentation says of $():

The most commonly used utility method is without doubt $(), which is, for instance, used pervasively within Prototype’s code to let you pass either element IDs or actual DOM element references just about anywhere an element argument is possible. It actually goes way beyond a simple wrapper around document.getElementById; check it out to see just how useful it is.

I think that a lot of developers don't get much passed the "most commonly used" part and incorporate it as their primary DOM element access tool.

At first glance (or even second glance) this may look harmless. I would contend that it is not.

is evil.

One thing that the DOM does is provide scoping for element names. document.getElementByID() (and it's even shorter, more tempting, prototype wrapper $() ) ignores all of that scoping, treating everything as if it's in one big global namespace. For small pages, fine, maybe not a huge problem. Just name everything carefully and you're set to use $( ) to reference things without much worry.

As soon as you start to look at larger pages, perhaps pages that are thrown together from a number of smaller templates, you can see problems though.

Imagine a simple "Log In" template:

< id="login">
< id ="loginForm" action="login.php" method="post">
< id="username" type="text">
< id="password" type="password">
< onclick="$('loginForm').submit();"> Log In < / button>
< /form >
< /div >

And now imagine a simple "Registration" template that consists of only a username field and a button that does a little ajax check for availability of that name. This is tacky, but that's not the point.

< id="register">
< id ="registerForm" action="register.php" method="post">
< id="username" type="text">
< onclick="checkAvailable($('username').value;"> Check Available < / button>
< /form >
< /div>

Both of these work fine on their own. But what happens when you put them on the same page? Bad things. Since both the username fields have the id "username" the $("username").value call in the onClick event handler in the registration form button is broken. When things work well on their own, but don't work well when you use them both at the same time (meaning, in this case, on the same page) that's a surefire sign that you've broken encapsulation.

Any template that haphazardly uses document.getElementByID() or $( ) is a danger to itself and others. No script should operate in a scope higher than the root tag of the template that it's included in. The question now becomes, how can this script scoping be enforced? There are a couple of ways that I can think of off of the top of my head, but both have their downsides.

The first, and in my opinion the worst way to do this (and unfortunately the one I've seen the most of..) is simply prepending a hopefully unique identifier to all element ids. So the two username fields from the above example might become something like login_username and register_username. I consider this to be a hack, since name collisions are still possible, just less probable. Also, it makes for overly verbose, ugly HTML in my opinion.

The second way would be to traverse the DOM from the root element of the template in order to get at the child element that you're trying to access. This still only decreases the probability of a collision instead of eliminating it entirely, but it makes for cleaner HTML than the first approach. It can, of course result in more verbose javascript, but using something like JQuery can make that javascript into reasonably concise and readable one-liners, so it's not too terrible.

I have to imagine that there's a better way to do this; a way that completely ensures that javascript doesn't accidentally operate outside of it's template's root element. The this pointer is a help in a lot of, but not every, situation. Does anyone know of a good general purpose method for restricting javascript to the proper score?

No comments: