Skip to content

Instantly share code, notes, and snippets.

@david-mark
Last active January 10, 2017 05:14
Show Gist options
  • Save david-mark/4349216 to your computer and use it in GitHub Desktop.
Save david-mark/4349216 to your computer and use it in GitHub Desktop.
Progressive Destruction

Progressive Destruction

Last time we looked at the decade-long (and failed) effort to reinvent event listener attachment. We saw that the inherent graceful degradation of HTML still beats the equivalent "unobtrusive" patterns and is certainly a better bet for prototyping an application than the usual suspect DOM libraries.

There's a myth that progressive enhancement is somehow a "better" technique for browser scripting, but the fact is that they are not mutually exclusive and each has its place. This may remind you of a similar argument regarding feature detection vs. browser sniffing, but it certainly doesn't apply in that context.

We left off with the start of a draggable toolbar that works with any manner or combination of pointing devices (including your fingers).

<div onclick="doCommand(event);" onmousedown="return startDrag(event, this)" ontouchstart="return startDrag(event, this, true)">
<button type="button">test1</button><button type="button">test2</button>
</div>

As a side note, you could also use INPUT[TYPE=BUTTON] elements, just don't forget to include the type. If you use BUTTON elements and omit the type, some browsers will create a submit button (INPUT[TYPE=SUBMIT]).

Assume that both buttons rely on at least one host method and remember that there is no telling which host methods are available and usable in a given browser. Somehow you've got to account for this. So what does a toolbar button that is incapable of executing its associated command look like?

<div onclick="doCommand(event);" onmousedown="return startDrag(event, this)" ontouchstart="return startDrag(event, this, true)">
<button type="button" disabled>test1</button><button type="button" disabled>test2</button>
</div>

Aha! And by no small coincidence, that works for the case where scripting is disabled or unavailable. There's a pattern to this stuff if you pay attention. ;)

Now, you will need to detect (and test in some cases) host features and enable just those buttons that should be expected to work. There was a time, many years ago, that you would want to detect whether the disabled property can be changed with script (or whether it is present at all), but that's really not a concern today (the technique worked in most browsers back then too).

<body onload="initializeCommands()">

If you can get past the "intellectual" concerns of mixing script with markup, you've got a winner. If the document has lots of images, videos or other cruft and you want the scripted commands to be available sooner:

<script type="text/javascript">
    // Give the browser a *chance* to finish parsing the last few tags

    window.setTimeout(initializeCommands, 1);
</script>
</body>
</html>

This sort of stuff goes in a header or footer and is forgotten forever. Well, unless you are bound and determined to make your life miserable with "unobtrusive" libraries like jQuery. The above techniques have always worked and very likely will always work. On the contrary, library efforts that spent years trying to reinvent this crucial wheel have turned out one square model after another. Why? Because they want you to use their code (and they can't can the above). It's not about "moving the Web forward" (obviously). It's about "look at me!" ;) For the end-users it becomes about "look for another site!".

Speaking of libraries, besides attaching listeners, sniffing out touch screens and "speeding up" page loading, what else does the typical do-everything DOM library help with? We covered pointers, but what about the other primary input device? Yes, many of the more "advanced" frameworks claim to "smooth out" keyboard input. Of course, they virtually all use browser sniffing to do so. The answer to that is four words: use access keys, stupid.

<div onclick="doCommand(event);" onmousedown="return startDrag(event, this)" ontouchstart="return startDrag(event, this, true)">
<button type="button" accesskey="1" disabled>test1</button><button accesskey="2" type="button" disabled>test2</button>
</div>

Yes, that will work in virtually every browser ever made, even some that reside on devices without keyboards. What's the perceived downside? Some browsers use Alt, some use Shift+Ctrl, etc. So what? If a user needs keyboard accessibility, they likely know how to make it work in their chosen browsers. If they simply want it (a power user perhaps), they can read the documentation or simply guess.

That works way better than any scripted solution as scripted solutions will always interfere with some browsers' built-in behavior. For example, if your script sniffs out Ctrl+Alt+3 and assumes it owns that combination, it may fail by either canceling the expected browser behavior or by doing something else in addition to the expected browser behavior. Access keys don't have this problem and require no script at all. On the other hand, if your script looks for just 3 (as is in fashion), the access keys will be unusable when the focus is on input controls (and the script will have to jump through various hoops to deal with that as well). But it's all moot as there aren't any cross-browser keyboard scripts out there (My Library's add-on likely comes closest). I mean, do you really want to tangle up keyboard input handling with browser sniffing? Think about that.

In the case where all of the command buttons are initially disabled, you don't have to worry about FoUC or premature user interaction (i.e. clicking a button before the supporting code is ready to respond). You may have to run some feature tests on load to determine what the commands will do and you don't enable the buttons until afterward. It's that simple.

One additional flourish that will make the document more app-like is to add TABINDEX attributes to take the buttons out of the tab order. You may have noticed that desktop application toolbars don't receive the focus. Obviously, you need to make sure all of your buttons have access keys and you may still piss off some keyboard user somewhere with this technique. It's the sort of thing that should be configurable at some level, preferably by the users.

<div onclick="doCommand(event);" onmousedown="return startDrag(event, this)" ontouchstart="return startDrag(event, this, true)">
<button type="button" accesskey="1" tabindex="-1" disabled>test1</button><button accesskey="2" tabindex="-1" type="button" disabled>test2</button>
</div>

As a side note, if you want your toolbar to act like a tabstrip, change one of the tab indices to 0, give it a CSS class to clear the bottom border and there you go. When they switch tabs, switch the 0 index. One tab at a time receives focus for the whole strip. After that you have to use script to deal with the arrow keys (that's how keyboard users change tabs). Thankfully, handling the arrows, as well as Esc, Enter, PgDn and PgUp is trivial (just be sure to confine such meddling to the widgets that need it). That's a topic for another time.

<div onkeydown="return navigationKey(event)" onclick="doCommand(event);" onmousedown="return startDrag(event, this)" ontouchstart="return startDrag(event, this, true)">
<button type="button" accesskey="1" tabindex="0" disabled>test1</button><button accesskey="2" tabindex="-1" type="button" disabled>test2</button>
</div>

You might think the markup structure is getting a bit bloated, but these sorts of attributes typically appear once per group of widgets (e.g. a container with one or more toolbars). Keyboard (as well as focus) tracking is virtually always done once for the entire document. It's not a concern, except for those intellectuals that can't abide by script in their document bodies. Should have ignored those from the start. ;)

Should also point out that all of these structures are eminently reusable by the power of copy and paste, server side scripting, macros, etc. Concerns about the number of characters typed are nonsense. You should be far more concerned about dropping in a giant black box of JS without the slightest clue of what it does (or how many visitors it will repulse while doing it). I've been told many times that that's how it's done in the "real world". :)

One alternative to these concise and simple techniques is to include some huge, complicated framework (e.g. YUI, Dojo, Ext JS) that swears it makes all of these tasks much easier, faster, concise, etc. But if they are just flailing around trying to reinvent long-since-invented wheels (and with lesser results), what's the point? The only point that mattered five or six years ago was that mobile devices with better browsers were on the horizon. They were going to be slower than their desktop equivalents, require battery power in some cases and would be generally crippled in much the same way as the "ancient" browsers of a few years prior. The "popular" libraries and frameworks of the time had a different take on that, seeing a world of Internet Explorer, Firefox, Opera and Safari. Mention of any other sort of browser was described as a "strawman" argument. Well, I suppose if your app was only expected to make it through the hour, that was an appropriate mindset.

At the time of this writing, jQuery is getting ready to ditch most of their efforts to make IE 8- behave. Was all just a waste of time, wasn't it? Efforts like Dojo and YUI are still trying to sign up Beta testers (er, users) for work on their futuristic vision, move the Web forward, etc. Developers still refuse to write any code that has ever been written in any "library" anywhere, preferring not to "reinvent wheels". It's really hopeless if everybody delegates all of their DOM scripting to incompetents. Sells a lot of dubious books and T-shirts, but only moves the proprietary apps forward. Anybody telling you different is either ignorant, lying or had been in a coma for the last decade.

@david-mark
Copy link
Author

Should note that I almost lost this Gist thanks to GitHub's overreaching scripted enhancements. Somehow wasn't logged in, so it saved it anonymously, but it wouldn't let me edit it so I could copy the raw text. No problem; hit the back button. Big problem. The text area was still there with my text inside, but it was completely unusable. It takes a really special script to break a browser (Chrome in this case) like that. Navigation and the related preservation of user data should be left to the browser. ;)

Managed to copy and paste the formatted text and put the markdown back, but came close to losing the whole thing due to overly ambitious JS programmers.

@david-mark
Copy link
Author

Next installment will deal with the rare cases where specific event types can't be counted on to bubble. Focus/blur are the primary examples. Your average ninja can tick off the various others (e.g. submit, reset, etc.) but, as we'll see, such memorization (as well as libraries that purport to save you the trouble), is mostly for naught.

@mkmcdonald
Copy link

mkmcdonald commented Dec 21, 2012

As a side note, you could also use INPUT[TYPE=BUTTON] elements, just don't forget to
include the type. If you use BUTTON elements and omit the type, some browsers will create a
submit button (INPUT[TYPE=SUBMIT]).

The default value for the type attribute of the button element is "submit" in HTML 4.01[[0]]. There should be no ambiguity.

Yes, many of the more "advanced" frameworks claim to "smooth out" keyboard input. Of
course, they virtually all use browser sniffing to do so. The answer to that is four words: use
access keys.

Precisely, as access keys are highly configurable. In my piece, Scepticism and the Web[[1]], I utilised access keys to navigate between pages.

As a side note, if you want your toolbar to act like a tabstrip, change one of the tab indices to 0,
give it a CSS class to clear the bottom border and there you go.

“CSS class” is incorrect; “HTML class” is not. In CSS, classes are mere selectors[[2]].

After that you have to use script to deal with the arrow keys (that's how keyboard users change
tabs). Thankfully, handling the arrows, as well as Esc, Enter, PgDn and PgUp is trivial (just be
sure to confine such meddling to the widgets that need it).

As a user that predominantly uses the keyboard, I hereby advise developers to avoid muddying the “scroll” keys (Page Up, Page Down, Home, End). Hackery rarely covers all permutations of those keys. Nor are we intelligent enough to cover of all them. Leave scrolling to the browser.

[[0]]: http://www.w3.org/TR/html401/interact/forms.html#adef-type-BUTTON
[[1]]: http://spoken.fortybelow.ca/Scepticism/
[[2]]: http://tantek.com/b/4MY1

@mkmcdonald
Copy link

Furthermore “Progressive Enhancement” is an authoring process, not a black box. “Graceful Degradation” is a run-time process triggered by a user. Both used in concert can yield flexible Web pages.

@david-mark
Copy link
Author

david-mark commented Jan 10, 2017

@mkmcdonald

As a user that predominantly uses the keyboard, I hereby advise developers to avoid muddying the “scroll” keys (Page Up, Page Down, Home, End). Hackery rarely covers all permutations of those keys. Nor are we intelligent enough to cover of all them. Leave scrolling to the browser.

Was referring to use with input widgets (e.g. number spinners, date pickers, etc.)

Furthermore “Progressive Enhancement” is an authoring process, not a black box. “Graceful Degradation” is a run-time process triggered by a user. Both used in concert can yield flexible Web pages.

Yes, they can. Didn't imply that they couldn't. To wit:

"There's a myth that progressive enhancement is somehow a "better" technique for browser scripting, but the fact is that they are not mutually exclusive and each has its place."

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