New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Non-class based example of customElement.define() #587

Closed
rektide opened this Issue Oct 15, 2016 · 79 comments

Comments

Projects
None yet
@rektide

rektide commented Oct 15, 2016

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?

@domenic

This comment has been minimized.

Contributor

domenic commented Oct 15, 2016

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.

@domenic domenic closed this Oct 15, 2016

@domenic

This comment has been minimized.

Contributor

domenic commented Oct 15, 2016

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.

@rektide

This comment has been minimized.

rektide commented Oct 15, 2016

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.

@domenic

This comment has been minimized.

Contributor

domenic commented Oct 15, 2016

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

@domenic

This comment has been minimized.

Contributor

domenic commented Oct 15, 2016

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

This comment has been minimized.

Contributor

rniwa commented Oct 15, 2016

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

This comment has been minimized.

Mr0grog commented Oct 16, 2016

(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

This comment has been minimized.

Contributor

rniwa commented Oct 16, 2016

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.

@Mr0grog

This comment has been minimized.

Mr0grog commented Oct 17, 2016

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

This comment has been minimized.

Contributor

rniwa commented Oct 17, 2016

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

This comment has been minimized.

Contributor

rniwa commented Oct 17, 2016

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

This comment has been minimized.

Contributor

rniwa commented Oct 17, 2016

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

@WebReflection

This comment has been minimized.

WebReflection commented Oct 17, 2016

@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

This comment has been minimized.

Contributor

rniwa commented Oct 17, 2016

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.

@WebReflection

This comment has been minimized.

WebReflection commented Oct 17, 2016

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.

@WebReflection

This comment has been minimized.

WebReflection commented Oct 17, 2016

@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

This comment has been minimized.

Contributor

rniwa commented Oct 17, 2016

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

@justinfagnani

This comment has been minimized.

justinfagnani commented Nov 10, 2016

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

@WebReflection

This comment has been minimized.

WebReflection commented Nov 10, 2016

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.

@trusktr

This comment has been minimized.

trusktr commented Dec 24, 2016

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?

@trusktr

This comment has been minimized.

trusktr commented Dec 24, 2016

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

@rniwa

This comment has been minimized.

Contributor

rniwa commented Jan 2, 2017

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.

@trusktr

This comment has been minimized.

trusktr commented Jan 6, 2017

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

This comment has been minimized.

trusktr commented Jan 6, 2017

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.

@trusktr

This comment has been minimized.

trusktr commented Feb 28, 2018

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.

@Jeff17Robbins

This comment has been minimized.

Jeff17Robbins commented Feb 28, 2018

@nnmrts

This comment has been minimized.

nnmrts commented Feb 28, 2018

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

@WebReflection

This comment has been minimized.

WebReflection commented Feb 28, 2018

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

@trusktr

This comment has been minimized.

trusktr commented Feb 28, 2018

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.

@holmberd

This comment has been minimized.

holmberd commented Mar 17, 2018

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.

@AndyOGo

This comment has been minimized.

AndyOGo commented Mar 21, 2018

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/

@trusktr

This comment has been minimized.

trusktr commented Mar 21, 2018

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

@trusktr

This comment has been minimized.

trusktr commented Mar 22, 2018

(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 😄 )

@trusktr

This comment has been minimized.

trusktr commented Mar 22, 2018

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

This comment has been minimized.

AndyOGo commented Mar 22, 2018

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

@trusktr

This comment has been minimized.

trusktr commented Apr 4, 2018

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.

@trusktr

This comment has been minimized.

trusktr commented Apr 4, 2018

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.

@trusktr

This comment has been minimized.

trusktr commented Apr 4, 2018

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?

@justinfagnani

This comment has been minimized.

justinfagnani commented Apr 4, 2018

@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'));
@trusktr

This comment has been minimized.

trusktr commented Apr 11, 2018

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

@rniwa

This comment has been minimized.

Contributor

rniwa commented Apr 11, 2018

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

@SerkanSipahi

This comment has been minimized.

SerkanSipahi commented Apr 11, 2018

... never ending story!!!

@trusktr

This comment has been minimized.

trusktr commented Apr 15, 2018

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?

@justinfagnani

This comment has been minimized.

justinfagnani commented Apr 15, 2018

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

This comment has been minimized.

Contributor

rniwa commented Apr 16, 2018

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

@morewry

This comment has been minimized.

morewry commented Apr 27, 2018

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.

@WebReflection

This comment has been minimized.

WebReflection commented Apr 27, 2018

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

@morewry

This comment has been minimized.

morewry commented Apr 28, 2018

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

developit added a commit to developit/preact-custom-element that referenced this issue Aug 24, 2018

Simplify + clean up class creation
Another option here would be to just use ES Classes directly. Babel transpiles them to a syntax that [doesn't work with Custom Elements](w3c/webcomponents#587), which limits options.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment