WICG / webcomponents

Web Components specifications
Other
4.38k stars 373 forks source link

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

Closed rektide closed 8 years ago

rektide commented 8 years ago

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?

rniwa commented 7 years ago

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.

WebReflection commented 7 years ago

subclassing builtins + species are overall broken, yes.

nnmrts commented 6 years ago

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.

WebReflection commented 6 years ago

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

trusktr commented 6 years ago

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

trusktr commented 6 years ago

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 commented 6 years ago

Adding a plug for Ractive, with great Mustache/Handlebars templates. We've mashed up Ractive with the webcomponents polyfill to give us a productive environment to build our apps. Kind of a lighter weight Polymer -- we call it Monomer.

On Tue, Feb 27, 2018, 9:51 PM Joe Pea notifications@github.com wrote:

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.

— You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHub https://github.com/w3c/webcomponents/issues/587#issuecomment-369103751, or mute the thread https://github.com/notifications/unsubscribe-auth/ACAWh4zMnr7Do5r4oSse4K4Rv62Kw5B7ks5tZL8rgaJpZM4KXuh2 .

nnmrts commented 6 years ago

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

WebReflection commented 6 years ago

@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 commented 6 years ago

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 commented 6 years ago

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 commented 6 years ago

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 commented 6 years ago

@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 commented 6 years ago

(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 commented 6 years ago

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. https://github.com/w3c/webcomponents/issues/423
  2. https://github.com/whatwg/html/issues/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 commented 6 years ago

@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 commented 6 years ago

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 commented 6 years ago

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 commented 6 years ago

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 commented 6 years ago

@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 commented 6 years ago

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

rniwa commented 6 years ago

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

SerkanSipahi commented 6 years ago

... never ending story!!!

trusktr commented 6 years ago

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 commented 6 years ago

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 6 years ago

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 commented 6 years ago

Someone wanna help with this one? https://github.com/WebReflection/document-register-element/issues/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 commented 6 years ago

@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 :man_shrugging:

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

morewry commented 6 years ago

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

trusktr commented 5 years ago

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

trusktr commented 5 years ago

@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 5 years ago

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

trusktr commented 5 years ago

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?

AndyOGo commented 5 years ago

@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 5 years ago

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

thecodejack commented 5 years ago

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 5 years ago

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.

thecodejack commented 5 years ago

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 5 years ago

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.

thecodejack commented 5 years ago

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.

daKmoR commented 5 years ago

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 🙈

thecodejack commented 5 years ago

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.

AndyOGo commented 5 years ago

@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

daKmoR commented 5 years ago

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

AndyOGo commented 5 years ago

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

xgqfrms commented 4 years ago

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 commented 4 years ago

@xgqfrms that's not a valid custom element name

xgqfrms commented 4 years ago

@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);
sapphous commented 3 years ago

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

bathos commented 3 years ago

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