Skip to content
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 · 100 comments
Closed

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

rektide opened this issue Oct 15, 2016 · 100 comments

Comments

@rektide
Copy link

@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
Copy link
Contributor

@domenic 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
Copy link
Contributor

@domenic 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
Copy link
Author

@rektide 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
Copy link
Contributor

@domenic 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
Copy link
Contributor

@domenic 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
Copy link
Contributor

@rniwa 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
Copy link

@Mr0grog 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
Copy link
Contributor

@rniwa 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
Copy link

@Mr0grog 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
Copy link
Contributor

@rniwa 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
Copy link
Contributor

@rniwa 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
Copy link
Contributor

@rniwa 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
Copy link

@WebReflection 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
Copy link
Contributor

@rniwa 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
Copy link

@WebReflection 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
Copy link

@WebReflection 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
Copy link
Contributor

@rniwa rniwa commented Oct 17, 2016

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

@justinfagnani
Copy link
Contributor

@justinfagnani 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
Copy link

@WebReflection 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
Copy link

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

@trusktr trusktr commented Dec 24, 2016

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

@rniwa
Copy link
Contributor

@rniwa 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
Copy link

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

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

@morewry
Copy link

@morewry 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
Another option here would be to just use ES Classes directly. Babel transpiles them to a syntax that [doesn't work with Custom Elements](WICG/webcomponents#587), which limits options.
@trusktr
Copy link

@trusktr trusktr commented Dec 2, 2018

@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
Copy link

@trusktr trusktr commented Dec 2, 2018

@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
Copy link
Contributor

@rniwa rniwa commented Dec 3, 2018

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
Copy link

@trusktr trusktr commented Dec 8, 2018

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
Copy link

@AndyOGo AndyOGo commented Dec 8, 2018

@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
Copy link
Contributor

@rniwa rniwa commented Dec 8, 2018

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

@thecodejack
Copy link

@thecodejack thecodejack commented Dec 18, 2018

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
Copy link
Contributor

@rniwa rniwa commented Dec 18, 2018

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
Copy link

@thecodejack thecodejack commented Dec 19, 2018

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
Copy link
Contributor

@rniwa rniwa commented Dec 20, 2018

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
Copy link

@thecodejack thecodejack commented Dec 20, 2018

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
Copy link

@daKmoR daKmoR commented Dec 20, 2018

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
Copy link

@thecodejack thecodejack commented Dec 20, 2018

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
Copy link

@AndyOGo AndyOGo commented Dec 20, 2018

@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
Copy link

@daKmoR daKmoR commented Dec 20, 2018

@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
Copy link

@AndyOGo AndyOGo commented Dec 20, 2018

@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
Copy link

@xgqfrms xgqfrms commented Jul 3, 2020

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
Copy link

@WebReflection WebReflection commented Jul 3, 2020

@xgqfrms that's not a valid custom element name

@xgqfrms
Copy link

@xgqfrms xgqfrms commented Jul 3, 2020

@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
Copy link

@sapphous sapphous commented Dec 2, 2020

@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
Copy link

@bathos bathos commented Dec 2, 2020

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Linked pull requests

Successfully merging a pull request may close this issue.

None yet
You can’t perform that action at this time.