Following some old bug links indicates to me this has all been gone over many 
times before. In particular:

- https://www.w3.org/Bugs/Public/show_bug.cgi?id=20913 discussion of class 
syntax, prototypes, <my-el> vs. <q is="my-qq">, and more
- https://www.w3.org/Bugs/Public/show_bug.cgi?id=21063 and related about 
upgrading (at one time upgrades *did* mutate the DOM tree)

I think perhaps the only new information is that, in the old ES6 class design 
with @@create, there was a feasible path forward where document.registerElement 
mutated the @@create to allocate the appropriate backing store, and left the 
constructor's "initialize" behavior (approximately it's [[Call]] behavior) 
alone. This would also work for upgrades, since you could just call the 
constructor on the upgraded element, as constructors in old-ES6 were not 
responsible for allocation. This would perform initialization on it (but leave 
allocation for the browser-generated @@create).

This was all somewhat handwavey, but I believe it was coherent, or close to it.

In contrast, the new ES6 subclassing designs have backed away from the 
allocation/initialization split, which seems like it torpedoes the idea of 
using user-authored constructors. Instead, we essentially have to re-invent the 
@@create/constructor split in user-space as constructor/createdCallback. That's 
a shame.

-----Original Message-----
From: Domenic Denicola [mailto:d...@domenic.me] 
Sent: Sunday, January 11, 2015 15:13
To: WebApps WG
Subject: Custom element design with ES6 classes and Element constructors

This is a spinoff from the thread "Defining a constructor for Element and 
friends" at 
http://lists.w3.org/Archives/Public/public-webapps/2015JanMar/0038.html, 
focused specifically on the design of custom elements in that world.

>> The logical extension of this, then, is that if after that 
>> `document.registerElement` call I do `document.body.innerHTML = <my-q 
>> cite="foo">blah</my-q>`
>
> Ah, here we go.  This is the part where the trouble starts, indeed.
>
> This is why custom elements currently uses <q is="my-q"> for creating custom 
> element subclasses of things that are more specific than HTMLElement.  Yes, 
> it's ugly.  But the alternative is at least major rewrite of the HTML spec 
> and at least large parts of Gecko/WebKit/Blink. 
>  :( I can't speak to whether Trident is doing a bunch of localName checks 
> internally.

So, at least as a thought experiment: what if we got rid of all the local name 
checks in implementations and the spec. I think then `<my-q>` could work, as 
long as it was done *after* `document.registerElement` calls.

However, I don't understand how to make it work for upgraded elements at all, 
i.e., in the case where `<my-q>` is parsed, and then later 
`document.registerElement("my-q", MyQ)` happens. You'd have to somehow graft 
the internal slots onto all MyQ instances after the fact, which is antithetical 
to the ES6 subclassing design and to how implementations work. __proto__ 
mutation doesn't suffice at all. Is there any way around this you could imagine?

Assuming that there isn't, I agree that any extension of an existing HTML 
element must be used with `is=""` syntax, and there's really no way of fixing 
that. (So, as a side effect, the local name tests can stay. I know how 
seriously you were considering my suggestion to rewrite them all ;).)

This makes me realize that there are really two possible operations going on 
here:

- Register a "custom element," i.e. a new tag name, whose corresponding 
constructor *must* derive *directly* from HTMLElement.
- Register an "existing element extension," i.e. something to be used with 
is="", whose corresponding constructor can derive from some other constructor.

I'd envision this as

```js
document.registerElement("my-el", class MyEl extends HTMLElement {}); 
document.registerElementExtension("qq", class QQ extends HTMLQuoteElement {}); 
```

This would allow <my-el>, plus <q is="qq"> and <blockquote is="qq">, but not 
<qq> or <q is="my-el"> or <span is="qq">. (Also note that element extensions 
don't need to be hyphenated, and there's no need for "extends" since you can 
get the appropriate information from looking at the prototype chain of the 
passed constructor.) document.registerElement could even throw for things that 
don't directly extend HTMLElement. And document.registerElementExtension could 
throw for things which don't derive from constructors that are already in the 
registry. (BTW, as noted in the existing spec, for SVG you always want to use 
element extensions, not custom elements.)

---

The story is still pretty unsatisfactory, however. Consider the case where your 
document consists of `<my-el></my-el>`, and then later you do `class MyEl 
extends HTMLElement {}; document.registerElement("my-el", MyEl)`. (Note how I 
don't save the return value of document.registerElement.) When the parser 
encountered `<my-el>`, it called `new HTMLUnknownElement(...)`, allocating a 
HTMLUnknownElement. The current design says to `__proto__`-munge the element 
after the fact, i.e. `document.body.firstChild.__proto__ = MyEl.prototype`. But 
it never calls the `MyEl` constructor!

This is troubling in a couple ways, at least:

- It means that the code `class MyEl extends HTMLElement {}` is largely a lie. 
We are just using ES6 class syntax as a way of creating a new prototype object, 
and not as a way of creating a proper class. At least for upgrading purposes.
- It means that what you get when doing `new MyEl()` is different from what you 
got when parsing-then-upgrading `<my-el></my-el>`.
- If we decide for the parser and/or for document.createElement to use the 
custom element registry when deciding how to instantiate elements, as was my 
plan in [1], it means that you'll get different results before registering the 
element as you would post-upgrade.

[1]: 
https://github.com/domenic/element-constructors/blob/master/element-constructors.js#L166-L189

(The same problems apply with <q is="qq">, by the way. It needs to be upgraded 
from HTMLQuoteElement to QQ, but we can only `__proto__`-munge, not call the 
constructor.)

I guess the current design solves this all by saying that indeed, the use of 
ES6 class syntax is a lie; you are only using it to create a prototype object. 
And indeed, you can't have a proper constructor, so use this createdCallback 
thing, which we will use to *make* a proper constructor for you, which behaves 
essentially like an upgrade does. So you have to save the return value of 
document.registerElement, and ignore the original constructor from your 
so-called class declaration. I guess those guys who put together the spec in 
the first place knew what they were doing ;).

I was hopeful that ES6 would give us a way out of this, but after thinking 
things through, I don't see any improvement at all. In particular it seems 
you're always going to have to have `var C2 = document.registerElement("my-el", 
C1)` giving `C2 !== C1`. And you're always going to have `createdCallback` 
sticking around, with its awkward relationship to `constructor`. And you'll 
never be able to write anything sensible inside your class's constructor body, 
since it's not going to be used.

The only alternative I can envision is some kind of overhaul of what 
"upgrading" means, changing it to be e.g. removing and re-creating all relevant 
elements using the appropriate constructor this time before re-inserting them. 
But that seems drastic, and potentially error-prone (e.g. if you saved a 
reference to the pre-upgrade element as a JS variable, all of a sudden you are 
holding a dangling HTMLUnknownElement that was removed from the tree. Do we try 
to transfer over all attributes, event listeners, etc. that might have been set 
on it? Ick.).

Well. At least now I understand the problem better.


Reply via email to