Re: Custom element design with ES6 classes and Element constructors

On Wed, Jan 14, 2015 at 3:47 PM, Domenic Denicola <d@domenic.me> wrote:

> I had a chat with Dmitry Lomov (V8 team/TC39, helped shape the new ES6
> classes design, CC'ed). His perspective was helpful. He suggested a way of
> evolving the current createdCallback design that I think makes it more
> palatable, and allows us to avoid all of the teeth-gnashing we've been
> doing in this thread.
>
> - Define a default constructor for HTMLElement that includes something
> like:
>
>   ```js
>   const createdCallback = this.createdCallback;
>   if (typeof createdCallback === "function") {
>     createdCallback();
>   }
>   ```
>
> - Detect whether the constructor passed to document.registerElement is the
> default ES class constructor or not. (By default, classes generate a
> constructor like `constructor(...args) { super(...args); }`.) If it is not
> the default constructor, i.e. if an author tried to supply one, throw an
> error. This functionality doesn't currently exist in ES, but it exists in
> V8 and seems like a useful addition to ES (e.g. as
> `Reflect.isDefaultConstructor`).
>

This seems useful to me too. Something is nagging me about it though, as if
there's a better way to help people with this use-case, but I'm not sure
what.


> - Define the HTMLElement constructor to be smart enough to work without
> any arguments. It could do this by e.g. looking up `new.target` in the
> registry. I can prototype this in
> https://github.com/domenic/element-constructors.
>

Isn't this at least a little future-hostile to things like `new
MyElement(attrs)`? Is there a way we could get back to that in the future,
in your mind?


> With these tweaks, the syntax for registering an element becomes:
>
> ```js
> class MyEl extends HTMLElement {
>   createdCallback() {
>     // initialization code goes here
>   }
> }
>
> document.registerElement("my-el", MyEl);
> ```
>
> Note how we don't need to save the return value of
> `document.registerElement`. `new MyEl()` still works, since it just calls
> the default `HTMLElement` constructor. (It can get its tag name by looking
> up `new.target === MyEl` in the custom element registry.) And, `new MyEl()`
> is equivalent to the parse-then-upgrade dance, since parsing corresponds to
> the main body of the HTMLElement constructor, and upgrading corresponds to
> proto-munging plus calling `this.createdCallback()`.
>
> Compare this to the "ideal" syntax that we've been searching for a way to
> make work throughout this thread:
>
> ```js
> class MyEl extends HTMLElement {
>   constructor() {
>     super();
>     // initialization code goes here
>   }
> }
>
> document.registerElement("my-el", MyEl);
> ```
>
> It's arguable just as good. You can't use the normal constructor
> mechanism, which feels sad. But let's talk about that.
>

I think this works *perfectly* (kudos!) as long as we don't want to support
constructor args being sent to readyCallback. As a "framework" hook, that
seems totally fine with me.


> Whenever you're working within a framework, be it Rails or ASP.NET or
> Polymer or "the DOM", sometimes the extension mechanism for the framework
> is to allow you to derive from a given base class. When you do so, there's
> a contract of what methods you implement, what methods you *don't*
> override, and so on.


Indeed. I've working on both Rails and Ember, and in both cases, it's *very
rare* to subclass from the internal constructor. There's usually a hook (or
event) API that subclasses can use so that the superclass implementation
can provide a more ergonomic API to implementations.


> When doing this kind of base-class extension, you're implementing a
> specific protocol that the framework tells you to, and you don't have
> complete freedom. So, it seems pretty reasonable if you're working within a
> framework that says, "you must not override the constructor; that's my
> domain. Instead, I've provided a protocol for how you can do
> initialization." Especially for a framework as complicated and full of
> initialization issues as the DOM.
>

Agreed.


> This design seems pretty nice to me. It means we can get upgrading (which,
> a few people have emphasized, is key in an ES6 modules world),


Can you say more about why same-identity upgrading is critical to the
design (as opposed to dom-mutation upgrading)? I asked up-thread but didn't
get any takers.


> we don't need to generate new constructors, and we can still use class
> syntax without it becoming just an indirect way of generating a `__proto__`
> to munge later. Via the isDefaultConstructor detection, we can protect
> people from the footgun of overriding the constructor.
>

This is pretty nice and pretty usable, I agree.


> On a final note, we could further bikeshed the name of createdCallback(),
> e.g. to either use a symbol or to be named something short and appealing
> like initialize() or create(), if that would make it even more appealing :)
>

I think symbols are actually pretty important here.

To elaborate, I envision a future where Ember tries to move its Component
API to be "just a DOM subclass". So when you inherit from Ember's
`Component`, you're actually inheriting from a DOM `HTMLElement` too.

document.register('my-element', class extends EmberComponent {

});


But Ember already has a whole suite of methods and properties on
`Ember.Component`, some of which may accidentally conflict with these
callbacks (now and in the future).

More generally, once people start writing libraries of HTMLElement
subclasses, our ability to add new callback names for all elements is going
to become pretty dicey, and we'll probably be forced into symbols anyway.
We may as well avoid a future inconsistency and just namespace DOM-supplied
callbacks separately from user-supplied properties and methods.

-- Yehuda

Received on Thursday, 15 January 2015 04:11:57 UTC