RE: alternate view on constructors for custom elements

From: Travis Leithead [mailto:travis.leithead@microsoft.com] 

> Something magical happens here. The use of super() is supposed to call the constructor of the HTMLElement class—but that’s not a normal JS class. It doesn’t have a defined constructor() method [yet?].

Yep. We'd need to define one; it's absolutely required. ES2015 classes that don't call super() in their constructor simply aren't allowed, which means inheriting from a constructor that throws (like HTMLElement currently does) is impossible.

https://github.com/domenic/element-constructors is a start at this.

> I’m trying to rationalize the custom elements previous design with the use of constructors. Basically, I think it should still be a two-stage creation model:
> 1. Native [internal] element is created with appropriate tag name, attributes, etc.
> 2. JS constructor is called and provided the instance (as ‘this’)
>
> #1 is triggered by the parser, or by a native constructor function. That constructor function could either be provided separately like it is returned from registerElement today, or in some other way (replacing the original constructor?). Since replacing the original constructor sounds weird and probably violates a bunch of JS invariants, I’ll assume sticking with the original model. 
>
> This makes it much safer for implementations, since the native custom element can always be safely created first, before running JS code. It also means there’s no magic super() at work—which seems to leave too much control up to author code to get right.

Ah! This is a big mistake!! You simply cannot do this split with ES2015 classes.

ES2015 classes are predicated on the idea that construction is an atomic operation, consisting of both allocation and initialization in one step. As such, class constructors cannot be called, only `new`ed. You cannot apply them with an already-created `this`, as if they were methods. This is fundamental to the class design; any other model needs to fall back to simply functions, not classes.

Indeed, the currently-specced design had a two-stage model---allocation/base initialization by the UA-generated constructor returned by document.registerElement, and author-controlled initialization by createdCallback(). This separation is, as you point out, makes things safer and easier to specify. And it is absolutely impossible to achieve if you insist on custom constructors, because in that case all allocation and initialization must happen together atomically. If we allow custom constructors, the allocation and initialization must both happen with a synchronous call to the custom constructor (which itself must call super()).

I take it when you said in [1]:

> I've discussed this issue with some of Edge's key parser developers. From a technical ground, we do not have a problem with stopping the parser to callout to author code in order to run a constructor, either during parsing or cloning. For example, in parsing, I would expect that the callout happens after initial instance creation, but before the target node is attached to the DOM tree by the parser.

you were not aware of this? Maybe now you better understand my follow-up question in [2],

> Can you expand on this more? In particular I am confused on how "initial instance creation" can happen without calling the constructor.

[1]: https://lists.w3.org/Archives/Public/public-webapps/2015JulSep/0161.html

[2]: https://lists.w3.org/Archives/Public/public-webapps/2015JulSep/0162.html


> Basic example of what I’m thinking:
>
> class XFooStartup extends HTMLElement {
>   constructor(val1, val2) {
>      this.prop = val1;
>      this.prop2 = val2;

This is invalid at runtime, since it fails to call super().

>   }
> }
> window.XFoo = document.registerElement(‘x-foo’, XFooStartup);

Why is XFoo different from XFooStartup? If I define a method in XFooStartup, does it exist in XFoo?

> // (1)
> var x1 = new XFooStartup(“first”, “second”);
> // (2)
> var x2 = new XFoo(“first”, “second”);
>
> Calling (1) does not create a custom element. Extending from HTMLElement is not magical, it’s just a prototype inheritance, as can be done today. super() would do whatever super() does when the super class has no defined method to invoke.

(It would throw, in other words.)

> x1 is a regular JS object with a .prop and .prop2.

This can't be true. Either x1 doesn't exist, because you didn't call super() and so `new XFooStartup` threw. Or x1 called super(), and it is no longer an ordinary JS object, because by calling super() (which is basically shorthand for `this = new super()`) you have made sure that it is a true allocated-and-initialized-by-the-UA HTMLElement.

> Calling (2) runs the platform-provided constructor function which internally inits a new HTMLElement in the C++ (or could be Element if in XML document?). Then the platform immediately (synchronously) invokes the provided constructor function as if:
XFooStartup.apply(newHTMLElementInstance, argsPassedThroughFromCall);
> so that XFooStartup’s constructor operates on the ‘this’ being the instance created by the platform.

Since XFooStartup is a class, `XFooStartup.apply` will throw an error.

This also doesn't address how any methods defined in the XFooStartup class may or may not get over to XFoo.

Hope this clarifies things! I had a suspicion there was some fundamental misunderstanding here...

Received on Friday, 17 July 2015 18:20:25 UTC