Html: Custom elements - ES5 constructor support?

Created on 23 Aug 2016  路  17Comments  路  Source: whatwg/html

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.

Most helpful comment

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.

All 17 comments

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?

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

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

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!

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?

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();
}

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.

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

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.

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.

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.

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.

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

@thomaswilburn: You can use ES5 polyfil in older browsers as long as you call Reflect.construct as I noted in https://github.com/whatwg/html/issues/1704#issuecomment-241668187 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.

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

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.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

NE-SmallTown picture NE-SmallTown  路  4Comments

benjamingr picture benjamingr  路  3Comments

FANMixco picture FANMixco  路  3Comments

lazarljubenovic picture lazarljubenovic  路  4Comments

NE-SmallTown picture NE-SmallTown  路  3Comments