HTMLElement.register--giving components tag names

"Components" (see
http://wiki.whatwg.org/wiki/Component_Model_Use_Cases for examples of
what I mean) need to present an API to script. For example, a
"contacts" component might want to expose a refresh() method that
pulls new contacts from the server. Components also need to hook up
internal behavior implemented in script; a split pane might need to
hook up a mouse listener on its splitter before it can really behave
like a split pane.

In script widget libraries today (YUI, Closure, etc.) this is easy
when the widget has been created from script: when the author calls
the constructor/factory method/whatever the widget can create DOM,
hook up event listeners, set up the prototype chain however it wants,
etc.

However things get really murky when authors start to use markup,
because then the parser is creating elements that are at best only
minimally functional until they're "enhanced." So today widget
implementations need to handle enhancing an existing element;
frameworks need to detect these unenhanced elements and invoke
something to enhance them; and authors needs to debug this unholy mess
when an unenhanced element escapes and their page breaks.

Wouldn't it be nice if the browser made this problem go away?

I think HTML itself shows us how life could be better: Consider the
HTML video element. It looks like a "subtype" of HTML element, with
all of the methods and attributes a HTML element has (via the
prototype chain), and adds methods and attributes specific for doing
video. When you do document.createElement('video') you get back an
object that is ready to use. There's no step to "enhance" it. When you
do p.innerHTML = "<video src=… controls=true>", you get a video
player. There's no flash of unstyled content. If you clone it and
insert the copy into the DOM again, all of the playback controls are
still wired up and responsive.

I'm proposing we add something that lets script extend the set of tag
names, so there is less of a bright line between elements defined in
the HTML spec and elements defined in script. Something like:

HTMLElement.register('x-contacts', ContactPicker);

The first argument is an element name. The second is a constructor
function, whose prototype property points to an object which
introduces the API for contacts (eg a refresh method) and is wired up
to HTMLElement.prototype.

When the parser encounters <x-contacts/>, it creates an object by
calling the ContactPicker function as a constructor. The constructor
can wire up any internal state it wants, like creating DOM and
attaching event listeners. When a script later retrieves the element,
say with document.querySelector('x-contacts'), the object created
earlier with ContactPicker is what it gets back. Since the constructor
is run as the parser is creating the element, there's no time when the
unenhanced element is available to script.

My proposal pretty much ends here: The component itself is just
implemented with script, DOM and CSS. There are no special hooks for
participating in layout (just use DOM and CSS) or form submission
(just create form controls using DOM) or security (same origin policy
still applies) or networking (just use WebSockets or XHR) or anything
else. There is no magic, other than teaching the browser about the new
tag name.

I'm suggesting that all registered tag names have to start with x-.
This is to give component authors a place to build without worrying
about conflicting with future HTML specs; the HTML spec won't define
elements with names starting with x-. (Component authors may chose x-
names that conflict with each other, though.) People working on the
HTML spec can study what kinds of components are popular in the wild
and consolidate the best ideas into new HTML elements. Page authors
can decide if and when to move their pages from their bespoke x-
components to the HTML alternatives, which should be better.

Pages that have to work in older browsers can use fallback content.
(This is exactly what the HTML spec advises authors to do with the
video element, incidentally.) In newer browsers the component script
can remove or hide the fallback content.

There's the question of what to do when there is a call to
HTMLElement.register('x-contacts', …) at some point after an x-contact
element has been parsed. We have to do something that makes sense for
async scripts. I think the solution is to replace the element, and the
recent discussion on this list about renameNode is interesting and
relevant.

One practical issue with this proposal is that creating a subtype of
HTMLElement in script (ie defining the ContactPicker function in the
above example) is hard/impossible. A couple of the problems are:

- Defining a subtypes in JavaScript is hard to begin with: You need to
create a function, and maybe set its object prototype up; you need to
create a prototype object and wire its object prototype up; you might
add a constructor property to the new prototype object; and you need
to remember to call your "base type" constructor to let it do any
setup it requires.

- Even if you do all this, HTMLElement.call(this), which you would use
to initialize your "base" type, doesn't work in the browsers I tried:
If HTMLElement even has a call method, it raises an exception if you
try to use it.

For now, I think we need something that (a) is implementable in
browsers today and (b) is harmonized with where JavaScript is going in
TC39 and where DOM bindings are going ala Web IDL.

The root cause of most implementation difficulties I see (I mostly
looked at WebKit) is that when a JavaScript wrapper object is created,
the browser wants to allocate a special wrapper object. It can't know
to do this in advance in the general subtyping case. So I suggest we
limit component authors to an "init" method that can create child DOM
and register event listeners. This is not unlike XBL's
enteredDocument, YUI's renderUI/bindUI, etc.

The browser can provide a factory that mints constructors that call
this init method at the appropriate time, but meet the browser's
idiosyncratic constraints of how wrapper object construction unfolds.
Using it would look like this:

function initContacts(element) {
  // Set up child DOM
  // Add event listeners
}
var ContactPicker = HTMLElement.extend({init: initContacts});

The resulting function can be used with HTMLElement.register, or even
called directly:

HTMLElement.register('x-contacts', ContactPicker);
document.body.appendChild(document.createElement('x-contacts'));  // works
document.body.appendChild(new ContactPicker());  // also works

You will notice that this says nothing about how prototypes are wired
up. It should. Maybe the argument to extend should have an optional
second field, proto, that specifies the new methods/getters/setters
that ContactPicker's prototype should introduce.

This is not a general subtyping mechanism! It is only designed for
setting up subtypes of HTMLElement for use with register. When
ECMAScript and the DOM bindings are sufficiently aligned,
HTMLElement.register can be opened up to accept constructors defined
using ordinary ECMAScript mechanisms for subtyping DOM interfaces.
Scripts can continue to use extend (it is pretty succinct) or
constructors set up their own way.

Lastly, I wanted to mention how this relates to the component model.
This is one small part. It could be used with the other parts--a
component constructor could set up its presentation aspects in shadow
DOM, for example. It could also be used by itself. As I illustrated
above it eliminates a lot of the difficulties of working with widgets
today.

Thanks for reading this far! These proposals aren't formal or
detailed. I would love to get feedback as I try to nail down some
specifics.

Received on Wednesday, 31 August 2011 05:34:10 UTC