WICG/webcomponents

Non-class based example of customElement.define()

rektide opened this issue · 100 comments

Hello,
I'd like for there to be an available, working examples of autonomous and customized Custom Elements made without use of the class syntax. The Mozilla MDN page for example shows a use of Object.create(HTMLElement.prototype) to create an autonomous custom element on it's Custom Elements page that satisfies this non-class based way of working, however that example doesn't work- it yields Uncaught TypeError: Failed to execute 'define' on 'CustomElementRegistry': The callback provided as parameter 2 is not a function. on customElement.define("my-tag", MyTag).

What is a valid syntax to use now, for creating autonomous and customized Custom Elements? Might we add some examples of such in to the spec?

It's not possible to use custom elements without ES6 classes. That was a design decision necessary to achieve consensus at the January face-to-face meeting.

Closing, since there's nothing actionable here, but happy to continue discussing in the closed thread.

I've updated the linked documentation to at least be up to date with custom elements v1. It still is on old shadow DOM however, and in general https://developer.mozilla.org/en-US/docs/Web/Web_Components looks very, very outdated and confusing. If anyone has time to update all the web components docs to the latest specs, that would be a great help to developers everywhere, I am sure.

I'd like to see non-class based JS become possible, hopefully in v2. Please re-open this as a request. Classes are syntax, statically constructed, which means we can't create components on the fly with code. This is a serious and frankly scary limitation.

For an example of use, if I wanted to generate components for, say, schema.org, the class based syntax means that I have to manually type out class definitions for ~650 components. Having normal, regular JS objects would have let me use code to generate new components. Please re-open this as an outstanding issue @domenic.

Sorry, this was a condition of getting consensus, and is now fully built in to the architecture of the feature. It cannot be changed.

I hope you're aware that you can generate classes dynamically just as easily as functions, so you certainly would not need to type those out. Classes are just as much syntax as functions are. If you don't know how do do this, please ask on StackOverflow, but not here.

rniwa commented

FWIW, you can use Reflect.construct to call HTMLElement's constructor. e.g.

function CustomElement() {
    return Reflect.construct(HTMLElement, [], CustomElement);
}
Object.setPrototypeOf(CustomElement.prototype, HTMLElement.prototype);
Object.setPrototypeOf(CustomElement, HTMLElement);

customElements.define('custom-element', CustomElement);

(Apologies if this issue has already been discussed elsewhere; I had entirely failed to consider it before and I haven’t seen it mentioned…)

Will this cause problems for existing JS codebases that use a WebComponents polyfill with a transpiler like Babel? For example, transpiling this code using Babel’s es2015 preset fails to work because the resulting JS doesn’t use Reflect.construct:

class TestElement extends HTMLElement {
  constructor () {
    console.log('Constructin’');
    super();
  }
  connectedCallback () {
    console.log('Connectin’');
  }
  disconnectedCallback () {
    console.log('Disconnectin’');
  }
}

customElements.define('test-element', TestElement);

const testInstance = document.createElement('test-element');
document.body.appendChild(testInstance);

I understand that native custom elements won’t be available in browsers that don’t already support ES-2015 class syntax, but if someone is using Babel + a polyfill for web components, it seems like they’d have a situation where their code works in older browsers (because the polyfill is active), but not in newer ones (because the polyfill just defers to the native implementation). That seems like a pretty big practical problem, but is it one you are concerned about here?

rniwa commented

It is true that if you're using Babel and polyfill, then the above code won't work out-of-box but that's true of any polyfill that got written before the standard is finalized.

There are various ways to workaround such issues, and probably the simplest solution is to wrap the thing you pass to customElements.define with a class. e.g.

function defineCustomElementInBabel(name, legacyConstructor) {
    var wrapperClass = class extends legacyConstructor {
        constructor() {
            var newElement = new Reflect.construct(HTMLElement, [], wrapperClass);
            legacyConstructor.call(newElement);
            return newElement;
        }
    };
    customElements.define(name, wrapperClass);
}

Obviously, this leaves new TestElement non-functional. An alternative approach is to replace super() call in TestElement by something special like:

class TestElement extends HTMLElement {
    constructor () {
        constructCustomElement(TestElement);
    }

with

function constructCustomElement(newTarget) {
        Reflect.construct(HTMLElement, [], newTarget);
}

There are dozens of other ways to cope with this limitations and that's really up to framework and library authors.

On a broader note, I don't think the standards process or API design in standards should be constrained by polyfills written before the general consensus on the API shape has been reached and at least two major browser engines have implemented it. Also, deploying a polyfill on production before the standards have become stable is almost always a bad idea.

It is true that if you're using Babel and polyfill, then the above code won't work out-of-box but that's true of any polyfill that got written before the standard is finalized.

I suppose I was really most focused here on the impact to existing codebases. It’s not as if that hasn’t been a consideration in other web standards, though I do understand that current usage of polyfills for custom elements (and especially v1-esque polyfills) is quite small.

On the other hand, there is a lot of Babel usage out there (the majority of non-trivial JS codebases I’ve worked on as a consultant over the past year have used it), and I hadn’t really expected that I’d need such an awkward and specialized method for creating a custom element with it. It may be further complicated in trying to find solutions that allow someone to inherit from a custom element provided as a third-party module, where the provider of the component may have solved the issue in their own way. As you noted, there are many ways to work around it.

Also, deploying a polyfill on production before the standards have become stable is almost always a bad idea.

I agree! I’ve just spent a lot of time shaking my head at bugs I’ve had to fix for clients because they shipped code that depends on an alpha/beta version of a library or a polyfill for a standard that hasn’t been finalized yet, so I’m sensitive to these kinds of decisions.

At the end of the day, I’m just a little frustrated at realizing the API for custom elements is less friendly than I had thought (again, entirely my fault for not reading as closely as I should have). I also understand that this is well past the point where anyone is willing to rethink it.

(I also want to be clear that I really appreciate the work being done here by everyone on the working group. Obviously I would have liked this issue to turn out differently, but I’m not complaining that this is some horrible travesty. The big picture is still an improvement for the web.)

rniwa commented

On the other hand, there is a lot of Babel usage out there (the majority of non-trivial JS codebases I’ve worked on as a consultant over the past year have used it), and I hadn’t really expected that I’d need such an awkward and specialized method for creating a custom element with it.

Okay. If you don't like a method, you can also define a specialized super class shown below. Obviously, this particular version of BabelHTMLElement only works with a browser engine with both ES6 and custom elements support but you can make it work with whatever polyfill as well.

function BabelHTMLElement()
{
  const newTarget = this.__proto__.constructor;
  return Reflect.construct(HTMLElement, [], newTarget);
}
Object.setPrototypeOf(BabelHTMLElement, HTMLElement);
Object.setPrototypeOf(BabelHTMLElement.prototype, HTMLElement.prototype);

class MyElement extends BabelHTMLElement {
  constructor() {
    super();
    this._id = 1;
  }
}

customElements.define('my-element', MyElement);
rniwa commented

Note that you can be more sleek with something like this (although I highly discourage you to override the native HTMLElement interface like this but sooner or later someone is gonna realize and do it so I'm gonna leave it here).

HTMLElement = (function (OriginalHTMLElement) {
  function BabelHTMLElement()
  {
    if (typeof Reflect == 'undefined' || typeof Reflect.construct != 'function' || typeof customElements == 'undefined') {
      // Use your favorite polyfill.
    }
    const newTarget = this.__proto__.constructor;
    return Reflect.construct(OriginalHTMLElement, [], newTarget);
  }
  Object.setPrototypeOf(BabelHTMLElement, OriginalHTMLElement);
  Object.setPrototypeOf(BabelHTMLElement.prototype, OriginalHTMLElement.prototype);
  return BabelHTMLElement;
})(HTMLElement);

class MyElement extends HTMLElement {
  constructor() {
    super();
    this._id = 1;
  }
}

customElements.define('my-element', MyElement);
rniwa commented

@WebReflection: In the case, you're still looking for a solution that works in both Babel + Polyfill and native ES6 + custom elements, see the comment above ^

@rniwa thanks for mentioning me but I'm not sure it's so easy.

Babel is plain broken when it comes to super calls and my poly already patches HTMLELement, so does the one from googlers.

I strongly believe this should be solved on Babel side, otherwise we're blocking and degrading native performance because of tooling on our way.

Tooling should improve and help, not be a problem.

rniwa commented

I've verified that both the ES6 and the Babel transpiled version works. The key here is to directly invoke Reflect.construct in your polyfill and not rely on Babel's super() call which, as you pointed out, is broken.

I'll play with your implementation and see how it goes. Maybe it'll make ife easier for everyone in this way so ... why not.

Thanks.

@rniwa it takes just new MyElement(); to fail with an illegal constructor error and the problem with babel is that even if you have that this._id set during constructor invokation, any other method defined in the class won't be inherited so no, your one does not seem to be a solution.

To summarize the issue:

class List extends Array {
  constructor() {
    super();
    this._id = 1;
  }
  method() {}
}

console.log((new List).method); // undefined

It doesn't matter if you have set something in the constructor if everything else is unusable

edit: in your case just add a method to your MyElement class and try to use it, it won't be there

rniwa commented

Oh, I see, that's just broken. Babel needs to fix that.

@rniwa just sent me to this issue. I'd like to share some of what we've done on the polyfill side of things...

First, we have a "native shim" to the Custom Elements polyfill so that ES5 constructors can be used to implement elements. There have been two versions of this shim:

The first version patched window.HTMLElement as a constructor function that used Reflect.construct along with this.constructor to emulate new.target. This has some prohibitive performance issues because 1) Reflect.construct is slow and 2) Reflect.construct isn't a real substitute for super() as it always creates a new instance, so this new HTMLElement constructor would always throw away the currently initializing instance and return a new Element instance. (old version: https://github.com/webcomponents/custom-elements/blob/b43236a7da0917ea938b6cb1aa3116caaeb6e151/src/native-shim.js )

The new version patches up the CustomElementRegistry API to generate a stand-in class at define() time and define that, and then keep it's own registry of user-defined ES5 constructors. It then does some shuffling for initialization. This approach is much faster and incurs only a 10% overhead over native CEs. The new version is here: https://github.com/webcomponents/custom-elements/blob/master/src/native-shim.js

There are some caveats that I list in the comments of the shim:

  1. All constructors in a inheritance hierarchy must be ES5-style, so that they can be called with Function.call(). This effectively means that the whole application must be compiled to ES5.
  2. Constructors must return the value of the emulated super() call. Like return SuperClass.call(this)
  3. The this reference should not be used before the emulated super() call just like this is illegal to use before super() in ES6.
  4. Constructors should not create other custom elements before the emulated super() call. This is the same restriction as with native custom elements.
  1. is a restriction because ES5 constructors cannot emulate super() and call into an ES6 constructor. 2) is just making ES5 constructors slightly more spec-compliant with ES6 constructors and required because HTMLElement sometimes returns an object other than this. I've worked with the major compilers to get their class transformations to implement this properly. Babel already worked. TypeScript has just fixed this, and Closure's fix is in review now. 3) is just respected the TDZ for this even in ES5 constructors. This shouldn't be something that authors need to care about if they write ES6. 4) is the same restriction that native CEs have.

What this means for Custom Elements authors is that everyone should write and distribute ES6 classes and let applications do any compiling down to ES5 that they need. This is a little different than the current norm of writing in ES6 and distributing ES5, but it will be necessary for any libraries that extend built-ins - Custom Elements aren't really unique here. Apps can either send ES5 to older browsers and ES6 to newer browser, or ES5 to everything using the shim.

Object.setPrototypeOf(this, elementProto) per each custom elements is just 10% slower?

Because I've proposed that already in the related Babel bug (since Babel is bugged for this and every other native constructor call) and they told me they didn't want to lose performance.

It looks like they delegated to you their transformation problem I've already said how to solve.

Thanks for sharing anyway, but I'm not sure this is the right way to go.

First, ES6 classes have a ugly static limitations (permanently engrained super references), and now we can't use ES5 classes in custom elements? What if we generate those classes from a class library? This is not ideal. The following should NOT give an error:

function BarBar() { HTMLElement.call(this); console.log('hello'); }
BarBar.prototype = Object.create(HTMLElement.prototype)
customElements.define('bar-bar', BarBar)
document.createElement('bar-bar')

Output:

Uncaught TypeError: Failed to construct 'HTMLElement': Please use the 'new' operator, this DOM object constructor cannot be called as a function.

and

function BarBar() { var _this = new HTMLElement(); console.log('hello'); return _this }
BarBar.prototype = Object.create(HTMLElement.prototype)
customElements.define('bar-bar', BarBar)
document.createElement('bar-bar')

output:

Uncaught TypeError: Illegal constructor

and

function BarBar() { var _this = new HTMLElement(); console.log('hello'); return _this; }
BarBar.prototype = Object.create(HTMLElement.prototype)
customElements.define('bar-bar', BarBar)
document.createElement('bar-bar')

output:

Uncaught TypeError: Illegal constructor

Honestly, why?

Why is the web becoming inflexible? Why are we blocking the dynamic nature of pre-ES6?

rniwa commented

This is not about Web becoming inflexible. This is about using [NewTarget] internal slot. new HTMLElement doesn't work because localName cannot be determined inside HTMLElement's constructor.

I've made a number of suggestions to solve this problem, one of which was about passing the local name from createElement, custom element's constructor, and then to HTMLElement. In this world, we can do the reverse lookup from the local name to the constructor object, and construct the element. However, this approach allows an inconsistency between the the actual constructor of HTMLElement's constructor and what HTMLElement's constructor ends up creating. Furthermore, it requires the HTMLElement's constructor to be called with the local name as an argument, which many people argued are unnecessary and ugly. Optionally allowing this would mean that the behavior of HTMLElement's constructor would flip between two modes, which is also not ideal.

I feel like it may be a bad design for the localName string property to be coupled to specific semantics of the JavaScript language. I like that you tried to fix the problem; it would allow the end user of the API to pass in any valid JavaScript class, not just ES6 classes and I think that would be very beneficial because not everyone wants to use ES6 classes all the time.

Furthermore, it requires the HTMLElement's constructor to be called with the local name as an argument, which many people argued are unnecessary and ugly.

Definitely true, that would be ugly!

If I understand correctly, new.target doesn't work with ES5 classes because calling a super constructor in the form SuperConstructor.call(this) means that there won't be a new.target reference inside SuperConstructor, so when HTMLElement is used like that it won't have a new.target and therefore cannot look up the constructor in the custom element registry?

Maybe we can add something to JavaScript? What if we add a new method to functions similar to Reflect.construct and that takes a context object, and only works when the containing constructor is called with new.

function Foo(...args) {
  HTMLElement.construct(this, args) // or similar, and new.target in HTMLElement is Foo.
}

Foo.prototype = Object.create(HTMLElement.prototype)

customElements.define('x-foo', Foo)
new Foo

Aha!! I got it to work with ES5 classes using Reflect.construct! Try this in console:

function Bar() {
  console.log('Bar, new.target:', new.target)
  let _ = Reflect.construct(HTMLElement, [], new.target)
  _.punctuation = '!'
  return _
}

Bar.prototype = Object.create(HTMLElement.prototype)

function Baz() {
  console.log('Baz, new.target:', new.target)
  let _ = Reflect.construct(Bar, [], new.target)
  return _
}

Baz.prototype = Object.create(Bar.prototype)

Baz.prototype.sayHello = function() {
  return `Hello ${this.localName}${this.punctuation}`
}

customElements.define('x-baz', Baz)

const baz = new Baz

console.log(baz.sayHello())

And this inside the Baz.prototype.sayHello is as expected! So, problem solved! I am HAPPY! One just has to use that Reflect.construct pattern, and inside a constructor manipulate _ instead of this, which I don't mind doing. A downside is that it creates a wasteful object on instantiation because this isn't being used and then will get GCed after the constructor returns _, so double the object creation.

rniwa commented

If you can use Reflect.construct, just do that. I’ve stated it in #587 (comment).

Please go read the discussions above before adding a new comment.

Oops, you're right, but also to note that I'm using new.target which is important if the class hierarchy is deeper. Sorry! And Thanks!

Honestly all of these workarounds just make me not want to write web components. You're not doing a very good job at selling this technology. "Oh no, it would be inconvenient for the browser vendor to implement" is not a valid excuse.

@trusktr this kind of workaround must not exist in a standard, it's too messy. Also forcing the developers to use only es6 classes will throw a lot of potential web components adopters to other technologies and frameworks, turning the main objective of web components not reachable...

rniwa commented

Again, you can just use Reflect.construct to make it work with non-class constructors. See #587 (comment). In addition, Safari, Firefox, and Chrome all have been shipping with class syntax for more than a year.

@SerkanSipahi This is about the natural language. The idea of web components is that they're universal and you don't have to install a bunch of 3rd party dependencies a page to get them to "just work".

Each of us users now has to babel.
(ex: https://stackoverflow.com/questions/43287186/why-is-wecomponentsjs-custom-elements-es5-adapter-js-not-working )

Why would you or someone not distribute the es5 version of the needed .js libraries, such as components and shadow so that part is done for us? We would then just need to babel the acctual component, not the dependencies done N times, one per each remote project that uses standard components.

@rniwa thanks for es5 extending example with Reflect.construct, I was already going to abandon idea to use webcomponents to refactor messy legacy vanillajs/jq code.

@rniwa Simply using Reflect.construct doesn't solve problems.

Reflect.construct is available only in Edge 12+! That's a big (bad) deal because we need to support older systems through polyfills that can't be polyfilled properly, and Custom Elements v1 is relying on not-completely-polyfillable requirements in a day and age when we need it to be 100% polyfillable to make everyone's lives easier.

The strict enforcement of using new means that transpilers can not compile to ES5 without using Reflect.construct because Reflect.construct won't work in anything below Edge 12.

This is a bad because not everyone is off of IE 10 or 11 yet, and there's no way to transpile this to a format that doesn't use new.target or Reflect.construct.

What this means is that people transpiling this to ES5 and using a Custom Elements v1 and Reflect polyfills will get lucky that it works in IE10, but then they'll get the following error in new browsers like Chrome:

Uncaught TypeError: Failed to construct 'HTMLElement': Please use the 'new' operator, this DOM object constructor cannot be called as a function

For example, see how bad this is with Buble output. Run the output code in Chrome.

This is not good! It makes things very difficult, especially in a time when many people still have to support non-native-ES2015+ environments.


Sorry for posting this twice, I wanted everyone to see it.

Buble is "batteries included", meaning I can't configure it to use Reflect.construct only for certain envs like I probably can with babel-preset-env, which means I'd have to switch to Babel, then I'd have to start making multiple builds, which will get very ugly for library authoring.

Aha, I know what the best solution is (thought it's not that good):

I can tell app developers: if you want to register my Custom Elements, you need to detect the user's browser, then use document.registerElement in IE (with polyfill), otherwise you can likely use customElements.define in all the other browsers (including Edge) because they support Reflect.construct.

Now, that's uuugly!

The reason I'm having this problem is because I'm writing classes that I can use with Custom Elements v1 using constructors, but I need to support older browsers that don't have Reflect.construct, so I'm writing classes that work when registered with v0 or v1 (this seems like a reasonable thing for a Custom Element library author to want).

@trusktr this problem is why we created the custom-elements-es5-adapterjs, that I've mentioned here before: https://github.com/webcomponents/webcomponentsjs#custom-elements-es5-adapterjs

Wow Justin, that's an incredible hack (using Object.setPrototypeOf(this, ...) inside the native constructor. I hadn't thought of such a thing before.

The arrow function, const, and Map is still in the way though. If we're running this code in older browsers (which is probably true if we're compiling to ES5), it will throw a syntax error.

Luckily for me, I'm supporting IE11+, so I only need to change to non-arrow.

The shim is intended to be run on browsers with native Custom Elements, to allow ES5 elements. On other browsers just use the polyfill.

To make things easy for people (f.e. people learning HTML and barely JavaScript), things just needs to work without complications. As such, I will include this adapter in my library so that things are simply easy.

(I'm not saying it's ideal from a technical standpoint of manageability and flexibility, just that in most cases it is easier to just ship polyfills for end users, especially if the polyfill does nothing if the thing being polyfilled already exists).

I'm not a fan of telling people "hey, you're running in IE, use this, this, this, this, this, and this polyfill". That can be somewhat annoying for people, and sometimes turns them away from simply trying something they otherwise might've liked.

It's like page load times: if the page takes too long to load, people may just leave a site that they might've otherwise liked; the same concept.

What will this do in older browsers that have a window.customElements polyfill? Hmmm, also class syntax will trip IE11.

What's your recommendation for libs then? I don't want to tell people: "detect the browser on server side and send the script tag if needed" for example. I want to tell people, regardless of browser, "use this script tag for my library" and done.

Here's the funkiness I used on a recent angular + custom elements project to conditionally load the custom-elements-adapter. angular-cli will use webpack to dynamically inject polyfills at the end of your index.html, so that's how I load webcomponents.js after this bit has had a chance to run. Frankly it's not awesome, but it may be something that we can put into a webpack plugin so devs don't have to muck with it much :\

@robdodson that's an interesting trick. I did something else: I changed arrows to functions, and put the class definition inside an eval() and it worked that way without causing syntax errors.

But I have another problem: Apparently this all works great with Babel's ES5 output, but I get errors with Buble's ES5 output (I'm using Buble to transpile my lib). I decided not to muck with it for now and support Edge 13+, and maybe by the time the lib I'm making is actually ready for anything serious this adapter won't be needed anymore! 😆

Wow Justin, that's an incredible hack (using Object.setPrototypeOf(this, ...) inside the native constructor. I hadn't thought of such a thing before.

you were worried about Edge 12+, setPrototypeOf can't be polyfilled in IE < 11.

Anyway, the issue has been addressed and solved for Babel ages ago, without needing extra files.
https://github.com/WebReflection/babel-plugin-transform-builtin-classes

@robdodson

CSP environment

What's that?

rniwa commented

By default, CSP blocks the use of eval.

I see, thanks. That'd be yet another complication to worry about. For my case, I've become content on supporting Edge 13 and up with native Classes, Proxies, and Reflect.construct to do some cool tricks.

rniwa commented

It appears to me that all these transpilers that lowers ES6 to ES5 need to be fixed to not break subclassing, and that's the only remaining issue here.

subclassing builtins + species are overall broken, yes.

Dude, this is just insane. Reading this thread I have more questions than answers. I know, this comment is long and probably belongs on a personal blog instead of a github issue, but let me explain where I come from:

I'm developing front end things for four years now and the moment I started, AngularJS was the biggest thing for beginners, but React became more popular over time. The last few times I worked with AngularJS I really hated it and it's size and it's slowness and it's shrinking community. So I thought: "Hey, let's try React, right?" Unfortunately the way things work in React are just to complex or weird for me to comprehend, it seems. Also, I thought the whole JSX thing is unnecessary. I mean, since we are moving to creating everything in .js files, why do we still use html-ish syntax for it instead of directly creating elements with the DOM APIs we have? Why do we use something like styled components with literally the same CSS syntax instead of just doing it the Javascript-camelCase way (element.style)? There is so much parsing and transpiling going on in the background, I don't know why anyone ever thought, this is the way we should make web components in Javascript in the future. To be fair, I don't know how much it impacts the performance, but even just from a logical perspective, why don't we really only use Javascript?

So, although I really don't have the time for it, I started "newh" (never ever write html), a framework for creating components and elements only using Javascript syntax. And guess what, I wouldn't write this comment, if it was simple.

My final question is now: In a sane javascript environment, like es6+babel, what is the fastest and/or preferred modern way to create custom HTMLElements and append it to any other HTMLElement when needed? I'd really love to use es6 classes, but at the moment it's just not possible, it seems. You have to use the old document.createElement which does some weird HTMLUnknownElement stuff that seems unsafe and that I don't quite understand.

My dream is this:

import { Component } from "newh";
import myChildComponent from "./my-child-component.js";

const myParentComponent = new Component({
    tag: "my-parent-component",
    attrs: {
        id: "my-id",
        class: "my-class"
    },
    style: {
        color: "blue",
        backgroundColor: "red"
    }
}, [myChildComponent]);

document.body.appendChild(myParentComponent);

where the Component constructor takes two arguments, a general settings object and a single child component or an array of child components. The output would be:

<html>
    <head>
    </head>
    <body>
        <my-parent-component
            id="my-id"
            class="my-class"
            style="color: blue; background-color: red;"
        >
            <my-child-component>
            </my-child-component>
        </my-parent-component>
    </body>
</html>

To my understanding, something like this is only possible with document.createElement() and element.appendChild(), and not with extending the HTMLElement class. And with "possible" I mean possible while using babel to support older browsers. Imho, this isn't a babel problem. Babel mostly transpiles correctly. There's just a lack of tools and libraries for web components focusing on normal environments. It doesn't matter if this cool new standard is implemented in the newest of the newest browser versions, I'm still on Vivaldi (chromium v64) because the newest official chromium 32bit build for Linux is 1 year old and on v62, so this really is the newest browser I could get for this machine. Now consider the users we develop for, a lot of them have even older browsers and don't even know that there is a way to activate experimental features on some weird chrome:flags page.

This whole thing is just frustrating. I really like the idea of web components and doing more things on the Javascript side, but the way it is presented to us developers is just miserable. Like "Hey, we have a cool new standard/concept of doing things on the web here! Go and use it, it's super cool! Oh and yeah, for the last 5 years we didn't came up with any usable universal polyfill, shim or framework to actually work with it. Lol."

Now you could say: "Why arent you trying to do a polyfill then?" Well, then please first update the damn docs for these web component and customeElements and HTMLElement APIs. There is not even one useful page to learn what you can and cannot do and what you should be able to do. Everything we have are some small examples, using HTML code to implement components. Things are really moving too fast here. As always in the software world. But this time it's just crazy how little the creators and implementers of this standard/concept care, to make this transition as easy as possible.

My last few days were just horrible because of this. I thought I could quickly dive into this, but I can't. No one with a normal brain can. It's just too messy right now. Seems like I will use AngularJS for another year, or at least until this situation gets way better than it is now.

Godspeed, please don't mess it up, peace.

@nnmrts you can use the V0 poly described in here, or use the polyfill that Google AMP sponsored and used since the beginning to bring Custom Elements everywhere years ago (plus AFrame, StencilJS, etctera).

Once you have basics portable, you can easily create that newh thing you want.

Yet the direction of W3C should be to promote modern standards, not just old conventions for libraries authors. We have too many libraries already and all we need is better, and wider support, of native features.

@nnmrts it's a little complicated at first, but all the tools are currently available.

I've gotten my custom elements working in IE 10 (v1 elements using custmElements.define). What I do is transpile the modern code using Babel 6 along with babel-plugin-transform-builtin-classes, and for the Web Component polyfills I use webcomponents.js. Note that Babel 7 will feature that transform in core, but it hasn't worked for me yet, so I recommend sticking with Babel 6 for now.

It all works great, my elements work in IE 10 and Chrome 65, as well as relevant versions of all the other browsers, all with a single build.

By the way, React offers the ability to dynamically morph your DOM. This is something you don't get with vanilla Custom Elements. You'll end up doing in plain JS what React/Angular/Vue and other view layers already solve for you. In your example above, you created a static DOM, but that example doesn't morph the DOM (declaratively or even imperatively) based on state changes. There's real value in these frameworks.

Furthermore more, Vue and React can be compiled at build time, and at runtime the DOM manipulation is pure JS under the hood, no parsing at runtime.

@trusktr

Furthermore more, Vue and React can be compiled at build time, and at runtime the DOM manipulation is pure JS under the hood, no parsing at runtime.

Oh, okay, thank you for that information, I didn't know that. So, I kinda get it now, but personally I still wish to have a direct javascript-only solution of doing these things. I thought that is the great nice thing of these new APIs. Welp, nevermind. :D

(if there is something like a javascript-only thing out there, please let me know ❤️)

@nnmrts hyperHTML is an example of 100% JS to morph/manipulate/update/react-to DOM through template literals. It also has light declarative components or HyperHTMLElement companion for CE. This is not the right place for these kind of discussions though, feel free to DM (open) me in Twitter, if needed.

One last thing, here's a non-exhaustive list of view layers, some of which are pure JS.

Btw, you can use pure JS with React, Vue, etc. Just don't compile, don't write JSX (HTML), and instead write stuff like React.createElement('div') in a React render function. See React without JSX, that's pure JS.

Since the class syntax currently has to be used to create custom HTMLElements, unless using Reflect.construct, but does not yet support extending HTMLElements with additional behaviour (since we want to avoid the creation of HTMLElements with no special behaviour). It leaves us with a mixed syntax code base that is quite ugly to look at and more difficult to read.

For example I still have code running that uses the following syntax which still works, but has one foot out the door:

var TestButton = document.registerElement('test-button', {
  prototype: {
    __proto__: HTMLButtonElement.prototype,

    createdCallback: function() {
      this.addEventListener("click", function(event) {
        console.log('test');
      });
    },
  },
  extends: 'button',
});
<button is='test-button'>test</button>

Whereas this does not:

class TestButton extends HTMLButtonElement {
  constructor() {
    super();

    this.addEventListener("click", function(event) {
      console.log('test');
    });
  }
}
customElements.define("test-button", TestButton, { extends: "button" });

Warning: At time of writing, no browser has implemented customized built-in elements (status). This is unfortunate for accessibility and progressive enhancement. If you think extending native HTML elements is useful, voice your thoughts on 509 and 662 on Github.

What I really miss in the whole journey of Web Components - which lasts for more than 7 years now - is a holistic approach to software engineering. What means holistic?

relating to or concerned with wholes or with complete systems rather than with the analysis of, treatment of, or dissection into parts

This practice of forcing ES6 class syntax really breaks this reasoned principle of holistic software. Guys if you design software API's this means you have to think about all possible consequences, various browsers, various JS engines, polyfills, transpilation, etc. etc. Why aren't you applying a holistic approach? I mean Web-Components aren't just a new JS library, they are a whole set of new browser APIs and have to work reliable.
It belongs to professional software engineering, it's part of your job, I really don't understand this standard of work.

Another product of yours I really disagree is releasing the <template> spec as living standard without any standard template feature like interpolation, conditionals, loops, partials you name it.
I mean you can find the definition of template in any dictionary, and there are lots of good examples out there in various programming flavors of what a template is. And which product do you release? You just provide a plain <template> tag and call it finalized living standard. I admit it would be an awesome geek joke at late night comedy. Unfortunately we (web developers) have to work with it.

On the other side I thank you, this way I get costumers asking me to poke with their problems, problems which could be avoided though...

If you don't change your approach, get properly organized and make sure to understand the technical words, your project of web components will fail broad adoption and leads to blog posts like:
https://dmitriid.com/blog/2017/03/the-broken-promise-of-web-components/

@AndyOGo I agree, things are not ideal how they are now. It's possible for implementors to make Custom Elements work with ES5 classes (anything is possible, it is just a collection of bits), but they choose to push the problem to user space. It is opposite of what should be: hard stuff solved at a lower level so everyone at the high level (users who know ES5, not just ES6+) can benefit.

Honestly, I use Web Components not because I like how the component system is implemented, but because I want the end result to be components that can be used everywhere, so I just put up with it. 😕

To make matters worse, I made myself a tool, lowclass, which let's me have "protected" and "private" members in my classes, and it is implemented with ES5 classes. This means I can't use it with Custom Elements v1 without going through great lengths. 😢

This means, I can't use a very useful feature of my tool, which is to make the API of my classes impenetrable, just because I chose not to use class. 😢 😢 😢

There are problems in the CE implementation that are solved by limiting what language features users can use, but I feel that instead the problems should be solved at a deeper level, so that JavaScript can be used in a free and dynamic way as much as possible in the user space. The dynamic nature of JS what makes it great, let's not take it away!

We should avoid coupling APIs to specific language features as much as possible.

Telling people to use Reflect.construct is practically the same as telling them to use class, both limiting and degrading developer experience and limiting what developers can achieve compared to the other options (for example making the Custom Element API work with ES5-style classes).

(Hey, but don't confuse the sad faces with my actual mood, I'm quite happy I get to make cross-platform components after figuring out how to deal with all the caveats 😄 )

It's a bummer we can't use any of the following libs to define our element classes.

These first ones are notable ones that are either popular or have cool features like protected/private members, multiple inheritance, concatenative inheritance, type specifiers for properties, interfaces, etc:

  1. https://www.npmjs.com/package/backbone (the extend helper)
  2. https://www.npmjs.com/package/proclass
  3. https://www.npmjs.com/package/joii
  4. https://www.npmjs.com/package/mozart
  5. https://www.npmjs.com/package/dejavu
  6. https://www.npmjs.com/package/classical
  7. https://www.npmjs.com/package/modelo
  8. https://www.npmjs.com/package/pjs
  9. https://www.npmjs.com/package/@stamp/it
  10. https://www.npmjs.com/package/dos
  11. https://www.npmjs.com/package/lowclass (shameful plug!)

And there's many more (I stopped perusing on page 6 of searching "class inheritance" on npmjs.com):

  1. https://www.npmjs.com/package/fiber
  2. https://www.npmjs.com/package/newclass
  3. https://www.npmjs.com/package/sjsclass
  4. https://www.npmjs.com/package/nature-js
  5. https://www.npmjs.com/package/baseclassjs
  6. https://www.npmjs.com/package/classkit
  7. https://www.npmjs.com/package/classy
  8. https://www.npmjs.com/package/es-class
  9. https://www.npmjs.com/package/selfish
  10. https://www.npmjs.com/package/ampersand-class-extend
  11. https://www.npmjs.com/package/cip
  12. https://www.npmjs.com/package/bike
  13. https://www.npmjs.com/package/js.class
  14. https://www.npmjs.com/package/pseudoclass
  15. https://www.npmjs.com/package/miniclass
  16. https://www.npmjs.com/package/jahcode
  17. https://www.npmjs.com/package/subclassjs
  18. https://www.npmjs.com/package/sclass.js
  19. https://www.npmjs.com/package/solv
  20. https://www.npmjs.com/package/legado
  21. https://www.npmjs.com/package/class-factory-js
  22. https://www.npmjs.com/package/chic
  23. https://www.npmjs.com/package/fac
  24. https://www.npmjs.com/package/cakes
  25. https://www.npmjs.com/package/clazzy
  26. https://www.npmjs.com/package/clazz
  27. https://www.npmjs.com/package/klass
  28. https://www.npmjs.com/package/o3
  29. https://www.npmjs.com/package/create-class
  30. https://www.npmjs.com/package/protect.js
  31. https://www.npmjs.com/package/exclass

There are some tools designed for use with native class:

  1. https://www.npmjs.com/package/mics
  2. https://www.npmjs.com/package/endow
  3. https://github.com/parro-it/private-class

Did you know there were that many? (I didn't even finish the search on NPM)

It would be great for new APIs not to discount the existing ecosystems, and for native class to be an option.

We could avoid problems (perhaps the most notorious problem of all new browser APIs):

  1. #423
  2. whatwg/html#1704
  3. https://stackoverflow.com/questions/45747646/what-is-the-es5-way-of-writing-web-component-classes
  4. https://stackoverflow.com/questions/41414034/transpiling-class-based-web-components-with-babel
  5. https://stackoverflow.com/questions/43287186/why-is-wecomponentsjs-custom-elements-es5-adapter-js-not-working
  6. https://stackoverflow.com/questions/43427281/custom-elements-v1-in-transpiling-from-typescript-to-ecmascript-5-failing-under
  7. https://stackoverflow.com/questions/43002652/how-to-get-polymer-2-0-es5-elements-working-with-v1-spec
  8. https://stackoverflow.com/questions/44121853/how-can-i-solve-the-error-uncaught-typeerror-class-constructor-m-cannot-be-inv
  9. https://stackoverflow.com/questions/41085635/typescript-2-1-custom-elements
  10. https://stackoverflow.com/questions/43520535/class-constructor-polymerelement-cannot-be-invoked-without-new
  11. https://stackoverflow.com/questions/47684104/is-there-a-custom-elements-polyfill-targeting-internet-explorer

@trusktr totally agree. Vanilla JS is already fantastic, in fact most people think it's just offers a prototype based paradigm. But JS is so much more, in fact in my opinion it's a multi-paradigm programming language. Which gives your lots of powers to implement your own sugar for object-oriented, functional, reactive you name it style of programming. There aren't many languages out there who gave you this opportunity. And Custom Element V1 just decided to cut into it 😱

Alright, so doing something like

  return Reflect.construct(HTMLElement, [], new.target)

with new.target just doesn't work because new.target is undefined. This is just a complete mess.

Okay, nevermind, the engine is in fact setting new.target, but in my case is a subclass was calling the super class with traditional ES5-style constructor.apply which makes new.target undefined.

So my above comment was wrong about new.target.

But this still shows how much of a pain point all of this is, and how easy it is to encounter problems.

To make HTMLELement subclasses compatible with plain ES5-style constructors that call their super constructor with .apply() or .call(), the HTML engine should allow this:

function MyEl(...args) {
    const el = Reflect.construct(HTMLElement, args, new.target)
    el.__proto__ = this.__proto__
    this.__proto__ = el

    // test:
    this.connectedCallback() // connected!
    console.log( this instanceof HTMLElement ) // true
}
MyEl.prototype = {
    __proto__: HTMLElement.prototype,
    constructor: MyEl,

    connectedCallback() {
        console.log(' ----- connected!')
    },
}
MyEl.__proto__ = HTMLElement

customElements.define('my-el', MyEl)
const el = document.createElement('my-el')
document.body.appendChild( el )

But the engine gives this error:

Uncaught TypeError: Failed to construct 'CustomElement': The result must implement HTMLElement interface

But everything about the instance created from the MyEl constructor implements the interface! There's not a good reason it shouldn't work. The engine could call connectedCallback if it just looks for the method which is there.

Why exactly can't we be allowed to do things like this?

@trusktr you're not returning the right object from the constructor. You're creating an HTMLElement, then implicitly returning MyEl, which is not an HTMLElement. Regardless of setting the prototype and what instanceof says, it doesn't wrap a native element object like HTMLElement does.

This works:

function MyEl() {
  return Reflect.construct(HTMLElement,[], this.constructor);
}

MyEl.prototype = Object.create(HTMLElement.prototype);
MyEl.prototype.constructor = MyEl;
Object.setPrototypeOf(MyEl, HTMLElement);

MyEl.prototype.connectedCallback = function() {
  console.log('my-el connected');
};
customElements.define('my-el', MyEl);
document.body.appendChild(document.createElement('my-el'));

Why doesn't setting the prototype like that work though?

rniwa commented

There's nothing wrong with the way you're setting up the prototype. What you're missing is return el; in the constructor.

... never ending story!!!

What you're missing is return el; in the constructor.

I don't think so, because I'm implicitly returning this which has the correct prototype (el) in it.

It'd be great if Custom Elements v2 doesn't have this issue, and is not restricted to a subset of JavaScript in terms of how we define classes.

If plain regular ES5 classes can work perfectly with the polyfilled version of Custom Elements v1, then surely it is possible to make the native version work too. No?

I'm implicitly returning this which has the correct prototype (el) in it.

The prototype isn't what makes a DOM node a DOM node - the JS object has to wrap a real DOM object created by the browser. Changing the prototype only changes the JS side, it doesn't create a DOM object.

I gave you an example that works, just use that.

rniwa commented

I don't think so, because I'm implicitly returning this which has the correct prototype (el) in it.

Okay. In your code, el != this so that's the problem. You can't return this. It's a different object from el. Because you're not using class constructor. this doesn't automatically get set to the result of calling super constructor (you're not even using super syntax so there's no way for the engine to figure this out).

Someone wanna help with this one? WebReflection/document-register-element#142

I remember when this call about custom elements and classes was made. I was willing to deal with it. I figured, the spec authors know what they're doing. It won't be a problem.

But it is a problem. It was a problem when y'all did it, it's still a problem today, and as far as I can tell the end is not in sight.

Every non-toy implementation I've attempted to do with custom elements for more than two years has been blocked by some variation of an issue stemming from the fact that custom elements are supposed to use a class.

I'm basically the lone advocate for open web standards and web components at the companies where I have these experiences. And as the lone engineer on the design systems I'm trying to build, I have very limited time. This is not helping my case and I'm this close to dropping it, possibly for good. I'm becoming unwilling to go out on a limb for web components anymore.

Not to mention we've lost generations of developers by making this too damn hard to use in real life. Someone hears about web components. They hear about the benefits of using the web platform and supporting open standards. They try to use custom elements. They run into a problem, they switch to React, and they have a great experience with it. We don't get a second chance to make a good impression.

@morewry your enemy here has a name, is called Babel, and every bundler using it carelessly.

I wouldn't blame standards right away, but surely I think standards have been blind for a long time.

hyperHTML and lit-html are an attempt to use standards the compelling way for developers, and yet standards don't really listen to devs 🤷‍♂️

Still a pretty irrelevant rant on this death thread though, good luck opening a new one.

I get that my problem isn't a bug in the spec, and has to do mainly with Babel's transpilation, but I think it's a valid perspective that the spec created the situation that led to my problem. That's why I want to express I think it was not a good call, especially given the consistent feedback since. This was one of the busiest conversations I've seen in that respect, so I chimed in here.

(I'd certainly open a new one if I thought there was any chance of it getting a different reaction from this one, but...I don't think that.)

@rniwa

Because you're not using class constructor. this doesn't automatically get set to the result of calling super constructor (you're not even using super syntax so there's no way for the engine to figure this out).

Seems like the problem should've been fixed in the engine without changing how JS works on the outside. It is possible!


I find myself here again because now I have a Custom Element defined where the following is happening and is very strange:

import MyElementClass from './my-el'

customElements.define('my-el', MyElementClass)

document.createElement('my-el') // works perfectly

new MyElementClass // TypeError: Illegal constructor

It works great in Chrome, Firefox, Safari, but I get this error in Electron spawned from Karma.

What on earth could be causing createElement to construct my element properly, yet using new doesn't work? No one answer that, I'm sure there's a handful of possibilities. The main point is that these sorts of problems are just bad.

Now I have to go spend some unknown amount of time solving a problem that no one should have to solve because the problem shouldn't exist.

The problems are a

... never ending story!!!

@SerkanSipahi ☝️👍


There is definitely an alternate course of history that JS (and HTML) could've taken so that all builtin classes were extendable in ES6+ without requiring any new language features.

@domenic

It's not possible to use custom elements without ES6 classes.

Yes, not possible for JS devs because of how the engine is implemented. But it's technically possible to change the engine implementation. (I'm not saying it is easy, but it is possible!).

I get that my problem isn't a bug in the spec, and has to do mainly with Babel's transpilation, but I think it's a valid perspective that the spec created the situation that led to my problem

@morewry great point!

rniwa commented

It works great in Chrome, Firefox, Safari, but I get this error in Electron spawned from Karma.'

That just sounds like a bug in Electron...

That just sounds like a bug in Electron...

Turns out I was goofing up. Load order is important. The following doesn't work:

const ES5Element = function() {}
Reflect.construct(HTMLElement, [], ES5Element) // Uncaught TypeError: Illegal constructor
customElements.define('es5-element', ES5Element)

while the following does:

const ES5Element = function() {}
customElements.define('es5-element', ES5Element)
Reflect.construct(HTMLElement, [], ES5Element)

Perhaps the error messages from the browsers could be more helpful.

For example Uncaught TypeError: Illegal constructor, HTMLElement. Did you forget to define a custom element before calling it with 'new'? instead of just Uncaught TypeError: Illegal constructor.


The reason I thought it worked in other browsers besides Electron was because I was either writing markup, or using document.createElement to create the elements, which works and the engine can upgrade them later. I was using new in Electron before defining the elements.

Would it be possible for the engine to special-case new MyElement to behave similarly to document.createElement?

@trusktr
To help you ignore order you should always construct you elements as soon as they resolve by using customElements.whenDefined(name);:

const ES5Element = function() {}

customElements.whenDefined('es5-element').then(function() {
  Reflect.construct(HTMLElement, [], ES5Element) // will never depend upon order
});

customElements.define('es5-element', ES5Element)
rniwa commented

FWIW, WebKit / Safari generates a slightly more helpful error message: TypeError: new.target does not define a custom element.

I am able to use Reflect.construct to migrate all my V0 components to V1. But now I am facing issues while I am creating new V1 components using ES6 classes(babelified).

Seems like all children with ES6 classes custom components are getting initialized correctly by the time browser calls connectedCallback but non class based child components which are built using Reflect.construct technique are not initialized in the connectedCallback of parent(which is ES6 class based component). Anyone faced this issue?

rniwa commented

Note that in general custom elements are upgraded in the tree order (prefix DFS), and connected & disconnected callbacks are enqueued in the tree order as well so in connectedCallback, for example, you shouldn't expect to be able to talk to your child elements / nodes. The recommended paradigm is for each child custom element to notify its parent. If you had to observe other kinds of changes or have to accept non-custom element, the recommendation is to use MutationObserver.

The recommended paradigm is for each child custom element to notify its parent.

Is there any implementation or wrapper to handle after my child components are ready and attached? MutationObserver is not something which completes my requirement. At present I am having setTimeOut in my base component class like following which runs ready method only once. But this doesn't seem to be correct way.

connectedCallback() {
   if (!this.isCustomInitialized) setTimeOut(()=>{this.ready()}, 0);
   this.isCustomInitialized = true;
}
ready() {
// write code which runs after children custom components are ready with shadowDOM
}
rniwa commented

MutationObserver is not something which completes my requirement

Why? MutationObserver lets you observe when child elements are inserted or removed. That's exactly when you should be running the logic to update your container element.

In general, the idea of all children being ready is flawed since an element can be inserted or removed dynamically.

I got your point of element insertion or removal but there is high probability the MutationObserver runs multiple times if there are multiple children that are active. Anyways I don't think need for lifecycle event after children being ready is a flaw. Even many major frameworks react (componentDidMount), vue (mounted), angular (afterView/afterContent) provides you similar functionality.

Even many major frameworks react (componentDidMount), vue (mounted), angular (afterView/afterContent) provides you similar functionality.

componentDidMount is the same as connectedCallback right? it has no concept of completeness of rendering.

Consider the following example:

// connected to dom =>
<foo-bar></foo-bar>
// is it ready?

// no as it needs to load some language data from an api =>
<foo-bar><p>hey foo</p></foo-bar>
// new we could say it's ready

It seems only you as a component author can make sure you are done done... and you can use whatever means necessary to do so (Promise, Callback, Event, ...)

PS: even more "crazy" example... assuming we take the finished loading of translations as being ready: an element that loads a form from an api which has some translations but also special input elements which need to load their own translations => so you could end up with the "form" being already ready (e.g. translations loaded) but the child elements are not (e.g. translations are not yet loaded) ... so to be truly ready you would need to check for every child elements readiness as well... => we are going down the rabbits hole 🙈

I haven't seen how react works internally much 😁 . I just tried logging componentDidMount and found that children's componentDidMount are called before parent's. But I am assuming due to virtual DOM that comes in react life cycle events, componentDidMount is happening after child DOM ready and connected as well.

Regarding example, true that it is getting complicated. I think that's where frameworks like react or vue won the game over custom elements/webcomponents.

@thecodejack
You have to be aware of one major caveat of custom elements, they are asynchronous. React is synchronous.

@daKmoR
So you can't say connectedCallback is the same as componentDidMount, there is this big difference of asynchronicity and others like V-DOM vs. the DOM.

I really recommend to carefully study the whole spec to become familiar with all the caveats involved:
https://html.spec.whatwg.org/multipage/custom-elements.html

@AndyOGo thx for clearing that up... so for the rendering componentDidMount will be sync.

However, the example with the loading of translations (via fetch to an external api server) is still valid right? or will componentDidMount wait until my xhr is done?

@daKmoR
You are welcome.

Regarding loading of translations, yes it's still valid.
Normally the workflow is as follows on componentDidMount() -> fetch() some data -> then call setState() -> will trigger componentDidUpdate, etc.

I really like this interactive diagram for React Lifecycle hooks and would wish similiar for custom elements:
http://projects.wojtekmaj.pl/react-lifecycle-methods-diagram/

what's wrong with that?

Uncaught SyntaxError: Failed to execute 'define' on 'CustomElementRegistry': "ufo" is not a valid custom element name

https://codepen.io/xgqfrms/pen/MWKrXpe?editors=1111

@WebReflection Thanks a lot, I had fixed that.

  <ufo-element>👍 customElements.define("ufo-element", UFO);</ufo-element>
  <ufo>👎 customElements.define("ufo", UFO);</ufo>

👍 customElements.define("ufo-element", UFO);
👎 customElements.define("ufo", UFO);

@domenic @morewry I just want to pop in and say that this is ridiculous. Javascript is a prototype-based language, and to get "consensus" it was necessary to block a critical feature from being possible using the prototype paradigm--and you refuse to fix it when it causes problems because of some sort of politics? It's nice that we have the option to use nice ES6 class syntax... but it's less powerful than raw prototyping and it's simply mind-boggling to me that my search for a solution led me to "there is none, because a committee banned it to force you to code their way even though the core features you're using aren't going anywhere".

Sometimes features are deprecated and things aren't backwards compatible; sometimes poor design introduces obstacles; but "sometimes a committee wants everyone to code a certain way for no particular reason and introduces obstacles to using perfectly supported features in a given context on purpose"? Is Javascript deprecated? Or is there a plan to eliminate prototypes from the language and move to a purely class-based approach--then perhaps add in strong typing and C++ template syntax? If so, that's crazy; if not, then the lack of support for prototype-based custom components is a serious bug.

Is there no way this can be brought up and addressed now that 4 years have passed? If the only reason there's no solution is that the design was intentionally crippled by bureaucratic fiat at a face-to-face meeting half a decade ago, perhaps it could be fixed now?

I guess I will now proceed to implement a bizarre, unreadable, inefficient workaround or introduce some hideous class definitions into my otherwise class-free library, for no reason whatsoever except that "the owners of the internet don't like prototypes".

@sapphous Class syntax is itself part of the “prototype paradigm,” but as others have mentioned, you don’t actually have to use it:

function NoClassSyntaxElement() {
  return Reflect.construct(HTMLElement, [], new.target);
  // or Reflect.construct(HTMLElement, [], NoClassSyntaxElement), I suppose, if you don’t care about subclassing
}

NoClassSyntaxElement.__proto__ = HTMLElement;
NoClassSyntaxElement.prototype.__proto__ = HTMLElement.prototype;

customElements.define('no-class-syntax', NoClassSyntaxElement);

console.log(new NoClassSyntaxElement().matches(':defined')); // true

This is useful if you need the super reference to be static but need to leave [[IsExtensible]] true (i.e., mimicking the behavior of platform interface constructors that inherit from others). That is pretty niche, mainly of interest for high-fidelity polyfilling stuff, but if for whatever reason you are averse to using class syntax to define your prototypes and object factories, it could serve you well too, and can be tucked away in a helper function.

Class syntax is a mechanism for defining prototypes declaratively alongside any initialization work involved in minting new instances. It doesn’t prevent manipulating the prototype and its properties; you can still do everything you might do in its absence. There is one (fairly obscure) primitive capability that class syntax has that is not exposed any other way* and there is one (fairly obscure) primitive capability that function syntax has that class syntax doesn’t**, but apart from these two things, they are exactly equal in capability.

Not trying to convince you to use em, just mentioning this because “less powerful than raw prototyping” seems like it might be a misconception (if you meant powerful in the sense of what fundamental capabilities they permit).

Not super important, but if curious, the two capability disconnects are...
  • * Class syntax is the only way to define a function whose [[ConstructorKind]] is "derived", which means you can skip or defer the initial access of new.target.prototype, which is technically observable. This is not possible with function syntax unless you count using a Proxy construct trap to achieve it.

  • ** Function syntax allows defining a function that has [[Construct]] but whose prototype property is initially writable, while class syntax does not.

When private fields land, the situation will change — that’s a pretty major primitive capability which, at least initially, will only be exposed through class syntax.