RSS
 

Archive for the ‘HTML 5’ Category

Update: HTML5 oninput event plugin for jQuery

24 Oct

About a year ago, I wrote my first special events plugin for jQuery — a cross-browser implementation of the HTML5 oninput event. A while afterwards, though, it became apparent that there were a multitude of issues with browsers that already implemented the event.

  • Opera 10.x doesn’t fire the event for cut, paste, undo & drop operations on <input> elements. They fixed this in Opera 11, though.
  • Safari 4.x and lower don’t support the event for <textarea> elements. The textInput event is supported on them for 3.1+, however.
  • Internet Explorer 9 forgets to fire the event when the backspace key is used to delete text. Other than that majorly horrendous oversight, they almost got it right.
  • Firefox has issues that make feature detection very difficult. In fact, detection is only possible via another Firefox proprietary feature, which I suppose is rather lucky.

With these problems stacking up, my code was stacking up too — how could I detect and work around each of these problems? On top of that, I couldn’t easily make the event work with delegate() or live(). Soon, it became apparent that I was going to have to completely rethink the approach I made in the original plugin.

Feature detection

The original code used feature detection to discover whether browsers supported the event. "oninput" in document.documentElement is enough for this in most browsers, but the aforementioned Firefox issues required an excellent little snippet from Daniel Friesen, which I slightly modified. It was a shame though, as this extra work bulked up the plugin a little more than I would have liked. Safari didn’t like our feature detection either, because even though our snippet told us the browser implemented support for the event, it doesn’t tell us that <textarea> won’t fire it.

I eventually decided that feature detection wasn’t going to work — graceful degradation, on the other hand, might. This involves binding to all known events that fire when an input element is modified, and then setting a flag to tell the event handlers whether or not they need to execute. For events that fire pre-DOM modification, we set a timer with timeout value of 0ms (this will be adjusted to browser’s minimum allowable value). The events that fire post-DOM modification cancel this timer. So, let’s say our browser is Internet Explorer 6, we have some text in our clipboard and we just pressed Ctrl+V in a <textarea> that has our events bound. For the Ctrl key, the onkeydown event sets the timer but when it executes the value hasn’t changed, so the handler does nothing. For the V key, the plugin will handle the events in the following sequence:

Pre-DOM modification
    onkeydown -> timer is set
    onpaste   -> previous timer is cleared, new timer is set

Post-DOM modification - DOM is modified, textarea's value is updated but element hasn't repainted yet
    onpropertychange -> timer is cleared, the handler is triggered

Opera 9, however, the best possible compatibility we can provide is via the onkeydown event, so that sets the timer which executes almost instantly, triggering our special event handler. In Safari 4, the textInput event fires post-DOM modification, so that clears the timer and triggers the handler, giving seamless `oninput` compatibility to the developer.

Late binding

No, not that kind of late binding. In order to mitigate the lack of event bubbling for our Internet Explorer polyfill, I opted to have the plugin not bind the input events to the element right away if it wasn’t an <input> or <textarea>. Instead, it binds several bubbling events — onfocusin, onmouseover and ondragstart — to the element. When one of those events fires, event.target.tagName is checked and if it is “INPUT” or “TEXTAREA” then we bind our input events to it. This allows delegate() to work really well in IE and I suspect, but haven’t verified, this is how jQuery is able to offer event delegation for non-propagating events such as onresize.

Problems with the new approach

Undoubtedly, the biggest problem is naming the special event. I can’t think of a way to have a special event named `input` that binds to the native `input` event and completely overrides it because, when we come to trigger the event later, we can’t. In my oh-so finite wisdom, I decided that the best solution I could provide was a differently named special event. So, when using the plugin now, you need to bind to "txtinput". $(selector).input(function () {}); will still work, though.

Another problem was teardown. Suppose we have a DOM structure like body -> div -> input — now, what happens if we bind to the <body> and <div> elements? Actually, this works completely as expected but, what if we unbind from one of those elements? Our teardown function needs to make sure that no other handlers are relying on our bound events before we remove them. The solution makes use of data and, every time an event is bound, increases an internal count to keep track. This counter is decreased for each of those elements in the teardown.

You can see the updated plugin at the original project page.

 

Scoping with HTML and inline event attributes

12 Dec

Just the other day on Stack Overflow, I saw a seemingly uninteresting question with the following code:




The user’s problem was that the call to click() wasn’t executing correctly, but if he changed the name of the function it did. I’ll admit that I wasn’t really in the mood for this type of question, so I brushed it off and moved on. The subject arose again in the JavaScript chat room where Pointy, a frequent flyer in the JS tags, was attempting to get to the root of the problem. My initial thought was that the user had made a mistake, but at the same time I thought it couldn’t hurt to check it out. So I fired up jsFiddle and pasted in the code. It failed silently, so I checked a couple of other browsers with the same result.

It was obvious what was going on at this point – the <button> element has a method on it named click() and the browsers were forcing the lexical scope of the function to include properties of the current object. I made a quick and dirty change to verify this:

    <button onclick="console.log(tagName)">try it</button><br />

Sure enough, the console output was BUTTON. I’m sure many experienced developers will be unsurprised at this, knowing this is the expected behaviour all along but, honestly, I’ve never encountered this before (probably due to how I like to attach my events via JS in the first place) and I don’t remember reading it in any specification. Pointy was able to find this portion of the HTML5 spec, however:

Lexical Environment Scope
  1. Let Scope be the result of NewObjectEnvironment(the element’s Document, the global environment).
  2. If the element has a form owner, let Scope be the result of NewObjectEnvironment(the element’s form ownerScope).
  3. Let Scope be the result of NewObjectEnvironment(the element’s object, Scope).


This indicates that the JavaScript equivalent for an element contained within a form would be something like this:

element.onclick = new Function(
    "event", 
    "with (document) { with (this.form) { with (this) { /* function code */ }}}"
);

I verified this by quickly testing getElementById without referencing the document object first. It worked just fine.

Now, after seeing all this, I’m left with several questions in my head all revolving around one important question — why? — why does anyone feel like it is necessary to do all this? I can’t think of any positive reasons for it to be this way, only reasons for it not to be this way:

  • The document object and the host element for the event are both very easily accessible from within the function’s scope.
  • NewObjectEnvironment() has taken a lot of flak over the years, the only thing that uses it is the with statement and Crockford likes to remind us that with statements are harmful, like in this example, we might not know if click() belongs to the element, the form containing it, the document or the window. It’s also very inefficient.


The only reason I can think of that this is in the HTML 5 specification is for backwards compatibility. This might be the way the coding dinosaurs wrote their HTML and JavaScript 15 years ago and this feature has survived since Netscape Navigator 2.0. In my opinion, this isn’t an excuse and it should have been deprecated and removed long ago. I don’t feel so badly about it for myself, though, I’ve got by for years without even encountering this behaviour and I’m sure it will never affect me in the future, unless I’m working with someone else’s code. Just one more reason to attach your event handlers using script, I suppose.

 
 

Effectively detecting user input in JavaScript

22 Sep

Like I said in my introductory post, my inspiration to start blogging came from Stack Overflow.  All too often, I see the same questions answered in the same way by different people and I think to myself, “No, it shouldn’t be done like that”.  Here’s one typical question:

“Hi, I’m trying to get the value of an input box as the user types into it.”

Almost instantly, you see the wrong answer appearing several times – “Use the onkeyup event handler.  It’s great and does all things!”.  The sad thing is that this is often the accepted answer.  “What’s wrong with this”, you may ask?  Well for one, the onkeyup event only fires when you lift your key from the keyboard.  The worst part is that these are often marked as the correct answer by the person asking the question.  This might not be a huge problem for you, but you might also be unaware that there can be a varying delay between when the value of the text input changes and when the user lifts their finger from the key.  Take the following example, type and might notice a slight delay before the text input’s value is copied to the div below it:

[iframe http://jsfiddle.net/jKrQK/show/light 100% 60px]
View source

Now hold your finger down on a key and let your OS’s input repeat trigger.  Notice how the div doesn’t update even though extra characters are added to the input?  This doesn’t feel very professional, does it?  Often, I provide a solution to these questions by means of a timer in the onkeydown event handler.  This still only handles input from a keyboard, but it feels less clunky as the update is almost instant.

[iframe http://jsfiddle.net/Gn7Rm/show/light 100% 60px]
View source

The timer is set with a 0ms value so that it is queued to be executed almost immediately.  I say almost, because all browsers have a minimum delay on setTimeout and setInterval (Firefox’s is 4ms), but it’s still a better solution than waiting for the onkeyup event and a much faster response for the user.  However, this still doesn’t deal with input that doesn’t involve a keyboard. What other types of input? How about context menu actions; cut, paste, undo and redo?   Drag and drop?  There have been events to handle some other kinds of input change for a while – several browsers support the onpaste, oncut and oncopy events – but they’re non-standard and some vendors are just too stubborn to implement them.

Enter the HTML 5 event, oninput.  This event really makes sense – one handler to rule them all, and browser support is looking great so far.  There’s just one exception to the list… wait for it… Internet Explorer!  So we’re back to square one.  Or are we?  Well, the oninput event can be emulated in Internet Explorer by handling the onpropertychange event.  This event will fire when any property changes on a DOM element, so all that’s needed is a simple check to make sure the property is element.value.  A proper implementation would check the input’s type property too, since on checkboxes and radio inputs the oninput event should fire when the state changes.  So let’s check out our latest solution, go nuts with copying, pasting, dragging and whatever!

[iframe http://jsfiddle.net/hQxxY/show/light 100% 60px]
View source

And there you have it.  If you need to support older browsers (note that this works in IE6+), you can use your favourite method of event detection to fall back to onkeydown with a timer. At least then you’ll have at least some of the desired functionality. The added benefit here is that this solution provides us with a method of validating user input when it is pasted or dropped into the box as well as typed, so you don’t have to wait until the change event to check for this.  I might touch on that in a later post, I think I’ve written enough for one day.

Note: There’s a bug in Opera 10.x where oninput doesn’t fire on certain elements for certain actions (like cut, copy, paste, drag & drop).  It does work fine for textarea elements, however, and this event still works better than the onkeyup even with this setback.  I’ve reported this issue to the Opera developers and hopefully they’ll fix it soon (edit: it’s fixed in Opera 11 alpha).  See also the bug affecting Firefox’s implementation of oninput.