Skip to content

Instantly share code, notes, and snippets.

@david-mark
Created December 16, 2012 20:54
Show Gist options
  • Save david-mark/4312841 to your computer and use it in GitHub Desktop.
Save david-mark/4312841 to your computer and use it in GitHub Desktop.
Unobtrusive JS === Unrealistic JS

Unobtrusive JS === Unrealistic JS

In general, Web developers want to be seen using the very latest and "greatest" Web technologies. We want to add buzzwords to our CV's as soon as they are coined and want to be considered "cutting edge" developers who are "moving the Web forward". Unfortunately, history takes a dim view of our exuberance and science ignores it completely.

Best to start at the beginning with DOM0, which came out in the 90's. Here we have a button that alerts when clicked:

<button type="button" onclick="window.alert('Hello world!')">Click me!</button>

Now, your typical luminary blogger/strategist/attention-seeker will decry such a structure as "ancient". In many cases, the technique predates their involvement in browser scripting; too old-fashioned to use on those cutting edge solutions that are in such demand by users. :)

But let's deconstruct this example. The alert will work in virtually every browser ever made. It won't appear with its OK button off-screen or blow up because some "modern" DOM method is missing or broken. Sure, you could create a DIV, set its inner text, add a button, position and display it yourself, but why? Typically the reason given is that Web designers-cum-developers aren't comfortable with anything that looks or acts the slightest bit differently from one browser to another. But that's their hang up as users don't typically browse the Web with multiple browsers side by side.

But I digress, the "onclick" attribute will raise the ire of forward-thinkers as it is "ancient" technology and mixes content with behavior. The former point is silly as, despite its ancientness, the above will work in virtually every scriptable browser ever made. The latter is completely ludicrous as most Web apps written between 1995-2010 are unusable today. These things always end up on the scrap heap in short order, requiring yet another "wheel" reinvention using just the very latest techniques and technologies. Odd that not reinventing the wheel is job #1 according to the standard industry pundit (that way we can quickly move the Web forward!) :)

Bypassing the "intellectual" concerns of mixing content and behavior (which seems appropriate given the example), let's look at the drawbacks of using the unfashionable "onclick" attribute. For one, we can only attach one listener per event type per element. But how often do you need to add two "click" listeners to the same button? If more often than never, you need to re-think your design strategy.

Back around the turn of the century, the community decided to "solve" this issue of multiple click listeners, once and for all. This was made possible by the availability of the more modern "addEventListener" method. Unfortunately, the way "forward" had/has a huge roadblock: Internet Explorer. Versions if IE before 9 and equivalent compatibility modes lack the "addEventListener" method.

The first "solutions" looked like this:

if (isIE) {
   el.attachEvent( ... );
} else if (isFF) {
   el.addEventListener( ... );
}

Of course, these unnecessarily excluded visitors for no reason at all (other than developer ego), prompting the first recorded cries of "we don't care about..." Right, we care deeply about adding multiple click listeners to buttons, but a few hundred thousand irritated and/or excluded visitors doesn't phase us a bit? This is progress for whom?

http://jibbering.com/faq/notes/detect-browser/

The learning curve is always the same: indirect object inferences came next.

if (window.ActiveXObject) {
   el.attachEvent( ... );
} else if (isFF) {
   el.addEventListener( ... );
}

Same story, just slightly less incompetent than the previous example. Still error-prone and exclusive for no reason.

Then, after several years of such "progress", we arrived at feature detection:

if (el.attachEvent) {
   el.attachEvent( ... );
} else if (el.addEventListener) {
   el.addEventListener( ... );
} else {
   window.alert('Dammit, this pattern doesn't fit!'); // Need a dynamic API to handle none-of-the-above cases
}

But wait, that's not the end of the quest for a cross-browser Hello World. Unfortunately, attachEvent and addEventListener are not identical. For one, the former does not pass the event object as the first argument. Well, easy right? Must move forward; so just add this:

e = e || window.event;

Then some smart egg pointed out that won't work for frames, alternate windows, etc. as they have their own window objects. Dammit. More complications followed, more code was piled on in the attempt to create the "perfect addEvent" function. This went on for years and culminated in a contest sponsored by noted strategist and forward-thinker PPK (who is famously critical of "ancient" event handler attributes). The "winner" was a young man called John Resig. Certainly we could expect great things from him in the future. :) Unfortunately, as you might have guessed, the solution proffered was ludicrous and exacerbated another well-known issue with attachEvent:

http://jibbering.com/faq/notes/closures/#clMem

Last I checked, jQuery still created such leaks, but no worries as they tacked on another hack to "fix" their self-imposed problem (see the pattern here). Unfortunately, their further attempts at a "perfect" solution multiplied the problems. For one, "unload" listeners break fast history navigation, multiplying the number of http connections per navigation. These sorts of "solutions" are what caused the Ajax craze in the middle of the last decade (in short, browser navigation was seen as a problem and adding huge, sputtering, complicated browser scripts was a popular way around it). But that's another (similar) story.

IE's "attachEvent" method also fails to set the "this" object to reference the element attached to the listener (the button in our example). For those interested in history, this is where the memory leaks crept in. Of course, the following works fine in virtually every browser ever made:

<button type="button" onclick="someFunction(this)">Click me!</button>

Much faster, far more concise (by about 100K!) and no worries at all. But then, it doesn't use the latest methods (which allow for multiple click listeners per button), so your fellow Web developers (assuming they are the standard forward-thinking zombies) may laugh. Of course, these jokes are on them. Years go by and they've only crept a few feet (usually in the wrong direction).

What's another issue with the original Hello World solution? Perhaps you need more than one button on your toolbar:

<button type="button" onclick="window.alert('Hello world!')"><button type="button" onclick="window.alert('Hello other world!')">

Or perhaps you need a dozen or two? This technique will certainly bloat your markup. Thankfully, there's always been a solution for that: it's called event delegation.

<script type="text/javscript">
function helloWorld(e) {
...
}
</script>

<div onclick="helloWorld(event)">
<button type="button" id="button1">Click me!</button><button type="button" id="button2">Click me!</button>
</div>

Note that we didn't have to deal with window.event as we passed the event object. We will, of course, have to extract the event target ourselves. This is the sort of "heavy lifting" that is often delegated to a huge, sputtering, complicated DOM library, but it is only a couple of lines:

function helloWorld(e) {
   var target = e.target || e.srcElement;
   
   // If target is a text node...
   
   if (target.nodeType == 3) {
   
       // Reference the parent node, which is the containing element
   
       target = target.parentNode;
   }
}

If you plan to reuse such code, you might create a function for it. Perhaps call it "getEventTarget". One cross-browser crisis averted. :)

Let's alert the ID of the clicked button:

window.alert(target.id);

All done. Of course, in a real application you would perform more useful actions based on the determined ID, rather than just alerting it. Regardless, this is the point where you can start thinking about writing your application (or enhancement). Wasn't too far to go, was it?

But wait, I know what you're thinking. That's fine and all for making an element "clickable" (a task for which jQuery and the like are often touted as useful tools). But what about more advanced behavior like drag and drop?

<div onmousedown="return startDrag(event, this)" ontouchstart="return startDrag(event, this, true)">
<button type="button" id="button1">Click me!</button><button type="button" id="button2">Click me!</button>
</div>

Note that we pass the event object and the reference to the "this" object directly to the two functions. The third argument is a flag that guarantees you are dealing with the touch API. The "startDrag" function I just used for a prototype is roughly a dozen lines long and handles several flavors of draggable widgets. It works in the latest touch devices, yet it also works in IE 5.5 (circa 1999). There's no library, no script loaders, no onload overhead or twitching load indicators, nothing for the browser to do but render content and leave it to the user. I actually did write all twelve lines "from scratch" (about three years ago). Obviously, it was ten minutes well spent. ;)

As an aside, if there are to be clickable elements inside of the draggable element, the "startDrag" function needs to bail out when they are targeted. In other words, if the target is a button and not the containing DIV, return without doing anything. If you fail to follow this advice, you will end up creating the "phantom click" problem. Note that you will have created the problem, not Apple. If you want to be able to drag the toolbar by its buttons, you will need more (and more complicated) code. This is where the "tap" click simulations came in and the library authors tried diligently for years to make them practical (they do waste a lot of time, don't they?) In short, you will need a wrapper that handles clicks as well as the usual mousedown/move/up simulations. Unfortunately, most of the touch/mouse efforts I've perused over the years lacked an "onclick" callback, rendering them unusable in their stated (general) context. Reading forum feedback on these things confirmed that they foul up exactly as one might expect (assuming they RTFM on Apple's ludicrous touch API).

As a further aside, there is a fair example of mouse/touch handling in the My Library touch add-on, which came out back when iPhones first invaded. The "attachTouchListeners" function is most of what you need, but it has a couple of drawbacks. It doesn't handle switching between mouse and touch on-the-fly (as users are allowed to do) and it lacks the "onclick" callback as well (ignore the nonsensical "attachTapListener" function). But regardless, for many contexts it is sufficient and for others it is a good cross-browser starting point. Was thrown together to prove that frameworks like "Sencha Touch" were plowing the same old territory, making the same old mistakes (e.g. UA sniffing) and thus were located nowhere near the "cutting edge". The iPhone and its (unfortunately awful) touch API were cutting edge (at the time). The remora-like libraries were just trying to attach themselves to the resulting hoopla. When you read breathless blog posts advocating long-dismissed techniques (like browser sniffing) to "solve" problems that don't really exist (except in the authors' heads) and avoid "reinventing wheels", you have to ask youself: am I really so stupid and lazy that I have to take the word of some cartoonish blogger? Never mind history and common sense, what does the cartoon visage on that popular blog tell you to think? :)

Of course, future-facing, "cutting edge" shops like Sencha (nee Ext JS) don't think you should make your life so easy. They want to do it for you. :)

This should look familiar:

if (isIOS || isAndroid) {
   ourAttachTouchListener( ... );
} else {
   ourAttachMouseListener( ... );
}

...And then this:

if (isIOS || isAndroid || isBlackberry) {
   ourAttachTouchListener( ... );
} else {
   ourAttachMouseListener( ... );
}

...And then this:

if (isIOS || isAndroid || isBlackberry || isWiiU) {
   ourAttachTouchListener( ... );
} else {
   ourAttachMouseListener( ... );
}

See the problem? Good, now avoid that problem and anyone pushing it as a "forward" move. You are better informed than that. ;)

As a side note, to keep up with the latest advancements in touch-enabled browsers, add this to the DIV for IE 10+:

style="-ms-touch-action: none"

Of course, in most cases, the rule would go in an external style sheet. Can't use Conditional Comments at this point, so will just have to trust in the inherently graceful degradation offered by CSS.

It seems MS put a little more into their touch API than Apple. It's advertised to actually work with at least some of the old mouse-based code. The CSS rule is supposed to prevent the default touch behavior so that scripts can do what they want. In other words, you don't want the viewport to scroll every time you drag and drop. Yes, you can accomplish the same thing with Apple's API by returning false from the touchstart listener. Unfortunately, you don't know at that point whether the user is clicking, swiping or what, so you have to then replace the rock-solid built-in touch handling with what is almost certainly going to be unreliable, scripted drivel. I mean, are we really that good that we can replace built-in, optimized device scrolling with general purpose scripts? I'm not; are you?

Also notice that we didn't need to call some ridiculous (and modern?!) wrapper like jQuery's "attr" to reference the "id" property. That's a good example of a library method that makes nightmares of even the simplest DOM operations. That is also another (similar) story.

With the popularity of mobile devices, script size has "suddenly" become a concern. The "leaders" are out in force telling us to jettison all of those old IE hacks to slim down our code. Of course, if you unceremoniously yank all of the IE hacks out of - for example - jQuery, you are left with a script that unnecessarily excludes a significant portion of the population, particularly corporate users (a segment known to have disposable income). Seems... crazy. :( Of course, sticking with the jQuery example, they have "solved" their own self-imposed problems by advocating the use of two different jQuery versions, with one wrapped in IE Conditional Comments. Not only is that just plain ridiculous (and hardly futuristic), but it is completely impractical (they've got enough trouble trying to maintain one script). This should be the tipping point for those jQuery users who are still conscious. Those guys wasted five years trying to "solve" the legacy IE browsers, ignoring suggestions from more experienced developers, only to mothball those efforts in the name of "moving the Web forward". In other words, they couldn't do it and gave up. ;)

Then there are those that advocate piling on more script to load other scripts on-the-fly, based on... browser sniffing mostly. Back to the egg. Don't follow them. :(

What's the explanation? From the very first time a button was scripted until today, Web developers have labored under the delusion that they will one day be writing all of the world's software. When the typical developer goes to write a toolbar, they want to make it "reusable" (a joke in this industry where "products" are basically treated as toilet paper) for every purpose and "better" than the desktop equivalent. This way we can move the world forward; proprietary applications out, open browser-based solutions in and all that. But a funny thing happened on the way to browser-based dominance: incompetence. It didn't work and now we are all using proprietary apps for virtually everything. Hardly shocking; this is how science works and the "ninjas" are powerless against its inexorable progress.

So for most Web developers, it's back to document enhancements. That's what JS and the DOM were designed to assist. To this day, that's mostly what they are used for and they are quite good at it. Stay away from the snake oil salesmen and perhaps you can learn just how good they are. Certainly this is contrary to the prevailing opinions in the blogosphere, where JS and the DOM are seen as "awful" or "not cool" or whatever and some framework-of-the-month is just what you need to make progress. Here's a hint: the bloggers and library developers are often the same misguided people. They don't care about helping you learn; quite the contrary, they want to keep you ignorant and reliant on them. Again, you have to ask yourself... :)

@mkmcdonald
Copy link

The “forward!” nonsense reminds me of the Democratic Party acolytes from the past American election.

DOM 0 handlers are perfectly acceptable. I attach them in JavaScript rather than HTML, but your point is taken. Apropos of your button snippet, I have recently discovered a technique to make buttons work without JavaScript. It follows:

CSS:

.button_form,
    .button_wrapper,
    .button_description
{
    margin: 0;
}

.button_wrapper
{
    border: 0;
}

.button_wrapper
{
    padding: 0;
}

.button_description
{
    display: none;
}

.fancy_button
{
    padding: 1em;
}

HTML:

<form action="./ValidLink"
    class="button_form"
    method="get"
    name="valid_link">
    <fieldset class="button_wrapper">
        <legend class="button_description">Description</legend>
        <button class="fancy_button"
            type="submit">Content</button>
    </fieldset>
</form>

The result is a fully functional form, styled with rich content. I have seen many people attempt to do the same with anchor or span elements. This solution beats them both.

@timdown
Copy link

timdown commented Dec 21, 2012

There's a slight error in this: an event handler attached via attachEvent() in IE does receive the event as the first parameter. It's DOM 0 event handlers that do not.

@david-mark
Copy link
Author

Tim,
IIRC, listeners attached with attachEvent don't get the event object as the first parameter. You have to deal with window.event. Are you testing in a true legacy environment (e.g. IE 8- on XP?) YMMV in the "equivalent" compatibility modes.

@david-mark
Copy link
Author

Tim,
Testing in compatibility mode confirms that an event object is being passed when using attachEvent. Don't have an XP box handy at the moment, but am curious to see if it was doing that all along. The "addEvent" wrappers from the early part of the century always had that e = e || window.event safeguard in there. Could be that they all had a DOM0 prong, but I doubt it. My Library's version, which is similar to what I used back then does have a DOM0 fallback, so my recollection on this point may be fuzzy.

Regardless, that bit was all a waste of time as DOM0 has worked fine since the 90's. Just never needed two click listeners on the same button and certainly don't believe in mashing up unrelated DOM scripts (which was the alleged justification for using addEventListener/attachEvent).

@david-mark
Copy link
Author

Matt,
"DOM 0 handlers are perfectly acceptable. I attach them in JavaScript rather than HTML, but your point is taken."

Either works. But for applications, I find that the scripts are often inexorably coupled to the document, so don't see the benefit of separating the two (despite about a million "unobtrusive or bust!" blog posts stressing the contrary). Certainly it's faster to let the browser attach the listeners. I don't do much of anything on load in such cases. Add a "ready" class to the BODY and initialize the disabled state of any toolbar buttons. That's about it.

el.onclick = function(e) {
e = e || window.event;

// ...
};

The - this - object is set, so nothing else to do but write the click handling code. :)

@timdown
Copy link

timdown commented Jan 31, 2013

I have real IE 7 running in an XP virtual machine and can confirm handlers passed into attachEvent do receive the event as the first parameter. I only discovered this a couple of years ago and was quite surprised by it.

@david-mark
Copy link
Author

Tim,

Yep. Apparently that "problem" was just oft-repeated folklore. Read something enough times and it becomes true. :) My own recollection was clearly faulty on that point.

It passes what appears to be a copy of window.event (though it very well could be a reference to it). Can never tell when comparing two host objects (another frequent source of confusion in this area).

@david-mark
Copy link
Author

Mike,

One other thing: if you take the "unobtrusive" approach to DOM0, you have to deal with the IE memory leak problem. It's not a big deal to avoid the issue and libraries like jQuery don't take care of it properly anyway (in the case of jQuery, it actually creates the problem for you!) As mentioned, trying to clean up the leaks on unload breaks very helpful browser functionality and won't do much good with the very popular (but almost always ill-advised) single-page site strategy.

@dawesi
Copy link

dawesi commented Nov 29, 2013

Of course you don't have to have a single page in a single page site strategy, we often use 'iframe' tech for throw away functionality, makes for a lighter dom and faster app. It's not an ideal world, but what is (it's javsacript)?

Never got how size matters when most of my customers use 4g connections (according to my metrics)

@david-mark
Copy link
Author

Dawesi,

Not just the download time that matters. The scripts have to execute, which drains batteries and bogs the whole page down for no good reason.

@mlhaufe
Copy link

mlhaufe commented Jan 18, 2015

A reference to the "contest" mentioned for future reference:
http://www.quirksmode.org/blog/archives/2005/10/_and_the_winner_1.html

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment