Re: Custom element design with ES6 classes and Element constructors

On Thu, Jan 15, 2015 at 5:11 AM, Yehuda Katz <wycats@gmail.com> wrote:

> 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?
>

If this is what is desired, HTMLElement can pass all its arguments to
createdCallback, i.e. call createdCallback(...args) in its constructor.

Since default constructors pass all their arguments upstream,
   new MyElement(attrs)
will pass attrs as arguments to HTMLElement constructor. HTMLElement
constructor needs no arguments itself, so it will pass all of those to
MyElement's createdCallback.



>
>> 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!)
>
Thanks!


> as long as we don't want to support constructor args being sent to
> readyCallback.
>

It seems to me this can still be supported, if I understand correctly what
do you want to to do.

Dmitry


> 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 06:09:54 UTC