RE: Custom element design with ES6 classes and Element constructors

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`).

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


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.

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

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), 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.

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 :)

Received on Wednesday, 14 January 2015 23:47:43 UTC