Custom elements - ES5 constructor support? #1704

Closed
thomaswilburn opened this Issue Aug 23, 2016 · 17 comments

Projects

None yet

4 participants

@thomaswilburn

https://html.spec.whatwg.org/multipage/scripting.html#custom-element-conformance

A parameter-less call to super() must be the first statement in the constructor body, to establish the correct prototype chain and this value before any further code is run.

How does this work for ES5 constructors, which can't call super() (but can have the prototype chain and this set up)?

Using the flagged version of V1 that's in Chrome as of a couple of weeks ago, it didn't seem possible to create an ES5 constructor that would pass customElements.define() without throwing an InvalidStateError. That's a huge problem for people who want to polyfill this in older browsers that don't support the class syntax, and seems weird given that ES6 classes are just sugar over the older constructor functions.

Without a good story on this, my team is going to end up sticking with the document.registerElement() polyfill for a long time, until IE 11 drops off our support matrix. I don't particularly want to, but if it'll work with transpiled code and customElements.define() won't, I don't see that we have a choice.

@thomaswilburn thomaswilburn changed the title from "A parameter-less call to super() must be the fi..." to ES5 constructor support in the spec? Aug 23, 2016
@thomaswilburn thomaswilburn changed the title from ES5 constructor support in the spec? to Custom elements - ES5 constructor support? Aug 23, 2016
@rniwa
Collaborator
rniwa commented Aug 23, 2016
@thomaswilburn

I don't believe that addresses the problem. Reflect.construct allows me to call the function as a constructor, but in the case of calling customElements.define(), I'm not the one calling the constructor (or rejecting it for not calling super())--the browser is doing that when it upgrades the element. If I'm wrong, can you provide some sample code to explain?

@rniwa
Collaborator
rniwa commented Aug 23, 2016
<!DOCTYPE html>
<html>
<script>

function MyCustomElement() {
    return Reflect.construct(HTMLElement, [], MyCustomElement);
}
MyCustomElement.prototype.attributeChangedCallback = function (name, oldValue, newValue) {
    alert(newValue);
}
MyCustomElement.observedAttributes = ['class'];
MyCustomElement.prototype.__proto__ = HTMLElement.prototype;
MyCustomElement.__proto__ = HTMLElement;

customElements.define('my-custom-element', MyCustomElement);

</script>
<my-custom-element class="hi">world</my-custom-element>
</html>
@rniwa
Collaborator
rniwa commented Aug 23, 2016

If you have Google Chrome Canary, you can try it on /Applications/Google\ Chrome\ Canary.app/Contents/MacOS/Google\ Chrome\ Canary --enable-blink-features=CustomElementsV1

@thomaswilburn

I had been trying in Canary, but not using Reflect.construct(). The current Chrome documentation states that it will reject any constructor that returns a different object, so I believed that using the constructor as a factory wasn't a possibility.

I'm on a Chromebook while traveling, so can't easily test flagged features at the moment, but will check on my Windows box when I get home. Thanks!

@thomaswilburn

I still need to test this in Canary, but in Chrome beta it's not possible to use HTMLElement as the target for Reflect.construct(), and I assume this will be true of other polyfilled browsers as well. Is there a good check to perform to see whether this is usable, or if I need to write additional boilerplate to set up the prototype chain?

@rianby64

Hello @rniwa !
I'm a bit confused...
Shouldn't be here something like...

MyCustomElement.prototype = Object.create(HTMLElement.prototype);
MyCustomElement.prototype.constructor = MyCustomElement;

instead of

MyCustomElement.prototype.__proto__ = HTMLElement.prototype;
MyCustomElement.__proto__ = HTMLElement;

?

And, what about if use

function MyCustomElement() {
    HTMLElement.call(this);   // this is just an idea for super();
}
@thomaswilburn

HTMLElement cannot be called directly as a constructor in most browsers, it throws an error. Canary will not accept that in a custom element definition.

@rianby64
rianby64 commented Aug 23, 2016 edited

@thomaswilburn thanks for the quote )) I didn't know about this restriction. The next question as consumer of this spec... should I care about the super? When I write

class MyCustomElement extends HTMLElement {
  constructor() {
    super();  // can I omit this? Can browser call it somewhere else?
  }
}

My motivation is that I describe a CustomElement that in fact is an HTMLElement, or an HTMLButton or anything else. What about if not set extends anywhere? What would be the default prototype for this CustomElement?

Please, this is just an idea... I'm newbie in JS ))

@thomaswilburn

While those aren't bad questions, it may make sense to ask them in a separate bug, as opposed to this one, which is trying to find out how to backfill the spec successfully in both legacy browsers and those that actually implement the V1 spec.

@rniwa
Collaborator
rniwa commented Aug 23, 2016

Shouldn't be here something like...

MyCustomElement.prototype = Object.create(HTMLElement.prototype);
MyCustomElement.prototype.constructor = MyCustomElement;

Please go read https://esdiscuss.org/topic/extending-an-es6-class-using-es5-syntax, and ask related JS questions on http://stackoverflow.com. This is an issue tracker for the HTML specification, not a support forum for every new feature in HTML / JS.

class MyCustomElement extends HTMLElement {
  constructor() {
    super();  // can I omit this? Can browser call it somewhere else?
  }
}

You can call Reflect.construct(HTMLElement, [], MyCustomElement) instead but you can't omit the call to HTMLElement constructor there since HTML tree construction cannot handle constructing a custom element resulting in either not a HTMLElement or a HTML element with a local name which is not equal to the given token.

What about if not set extends anywhere? What would be the default prototype for this CustomElement?

You could do:

class MyCustomElement {
    constructor() {
        return Reflect.construct(HTMLElement, [], MyCustomElement);
    }
}
customElements.define('my-custom-element', MyCustomElement);

Although this has a weird behavior that the constructed custom element would not have HTMLElement in its prototype chain whilst it's branded as HTMLElement. The UA would continue to treat it as a HTMLElement but author scripts need to do something like Element.prototype.getAttribute.call(element, "class") to invoke HTMLElement's method on the element.

@domenic
Member
domenic commented Aug 23, 2016

Closing, since as @rniwa noted this is not a support forum, and there is no spec bug being reported here.

Everyone should remember that every browser that implements custom elements also implements ES6 class syntax, with super(). The features are designed to work together and there is no desire to make things work with ES5 syntax or with older browsers, since the specification is made for newer browsers.

@domenic domenic closed this Aug 23, 2016
@thomaswilburn

I realize that your response is probably shorthand for a lot of the long-standing debate that happened while this spec was being hashed out, but that's... an unfortunate stance to take for those of us who would like to take advantage of this soon-ish, instead of several years from now.

@rianby64
rianby64 commented Aug 23, 2016 edited

@rniwa Thanks a lot for your meticulous answer. My intention wasn't to turn this issue into a forum-story. I just was curios why you mentioned __proto__?. As you noticed, I'm not a top-developer, and find very useful to ask to developers like you or @domenic about those things that I really want to understand. Thanks a million to @domenic too for his nice and laconic answers in other issues, and hope that this "indirect" support won't break. I really appreciate every word.

And, if talk about the possibilities, I understood clearly

the features are designed to work together and there is no desire to make things work with ES5 syntax or with older browsers, since the specification is made for newer browsers.

@rniwa
Collaborator
rniwa commented Aug 23, 2016

@thomaswilburn: You can use ES5 polyfil in older browsers as long as you call Reflect.construct as I noted in #1704 (comment) on new browsers that implements customElements.define since ES6 class is just a syntax sugar for ES5 class and Reflect.construct. I'd imagine that's what Polymer or any other library/framework would do.

@rianby64: I don't have any intention to judge anyone's knowledge here. I'm just saying that we can't answer every question you may have about custom elements or related technology beyond the scope of discussing the specification itself as we all have limited resources and time.

@thomaswilburn

@rniwa Both of the polyfills that I'm aware of are in fact having to monkey-patch the HTMLElement constructor (and its subclasses), because you can't call Reflect.construct(HTMLElement, [], XCustomElement) in older browsers: it throws an Illegal Constructor error, since it wasn't a callable function until this was implemented. The solution that works across polyfill/native seems to look something like this (although I need to test further when I get back to the US):

var XCustomElement = function() {
  try {
    var self = Reflect.construct(HTMLElement, [], XCustomElement);
    return self;
  } catch (_) {
    HTMLElement.call(this); // the polyfill will have monkeypatched the constructor
  }
}

It looks like this will be doable with the monkey-patch in place, but that's an ugly solution at best (welcome to the web, I guess). It seems ironic to me that I've found bugs commenting on this spec having too much "tutorial" content--as someone who's trying to get it to work in a real-world environment, I would argue that there's not enough.

@rniwa
Collaborator
rniwa commented Aug 23, 2016 edited

That's why I had the qualifier new browsers that implements customElements.define. Whatever polyfill people write need to take that into account.

It can be shown that one can write a framework that lets users of the said framework write code as such:

function MyCustomElement() {}
defineCustomElement('my-custom-element', MyCustomElement);

e.g.

function defineCustomElement(localName, elementInterface) {
    elementInterface.prototype.__proto__ = HTMLElement.prototype;
    elementInterface.__proto___ HTMLElement.prototype;
    if (window.customElements) {
        window.customElements(localName, function () {
            var newElement = Reflect.construct(HTMLElement, [], elementInterface);
            var returendElement = elementInterface.call(newElement);
            return returnedElement !== undefined ? returnedElement : newElement;
        });
    } else {
        // Polyfil code
    }
}

I'm going to unsubscribe from this thread because I feel like I'm just telling how to write a framework / polyfll, and that's not really my job.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment