-
Notifications
You must be signed in to change notification settings - Fork 378
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
Comments
It's not possible to use custom elements without ES6 classes. That was a design decision necessary to achieve consensus at the January face-to-face meeting. Closing, since there's nothing actionable here, but happy to continue discussing in the closed thread. |
I've updated the linked documentation to at least be up to date with custom elements v1. It still is on old shadow DOM however, and in general https://developer.mozilla.org/en-US/docs/Web/Web_Components looks very, very outdated and confusing. If anyone has time to update all the web components docs to the latest specs, that would be a great help to developers everywhere, I am sure. |
I'd like to see non-class based JS become possible, hopefully in v2. Please re-open this as a request. Classes are syntax, statically constructed, which means we can't create components on the fly with code. This is a serious and frankly scary limitation. For an example of use, if I wanted to generate components for, say, schema.org, the class based syntax means that I have to manually type out class definitions for ~650 components. Having normal, regular JS objects would have let me use code to generate new components. Please re-open this as an outstanding issue @domenic. |
Sorry, this was a condition of getting consensus, and is now fully built in to the architecture of the feature. It cannot be changed. |
I hope you're aware that you can generate classes dynamically just as easily as functions, so you certainly would not need to type those out. Classes are just as much syntax as functions are. If you don't know how do do this, please ask on StackOverflow, but not here. |
FWIW, you can use function CustomElement() {
return Reflect.construct(HTMLElement, [], CustomElement);
}
Object.setPrototypeOf(CustomElement.prototype, HTMLElement.prototype);
Object.setPrototypeOf(CustomElement, HTMLElement);
customElements.define('custom-element', CustomElement); |
(Apologies if this issue has already been discussed elsewhere; I had entirely failed to consider it before and I haven’t seen it mentioned…) Will this cause problems for existing JS codebases that use a WebComponents polyfill with a transpiler like Babel? For example, transpiling this code using Babel’s 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? |
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 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 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. |
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.
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.) |
Okay. If you don't like a method, you can also define a specialized super class shown below. Obviously, this particular version of 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); |
Note that you can be more sleek with something like this (although I highly discourage you to override the native 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); |
@WebReflection: In the case, you're still looking for a solution that works in both Babel + Polyfill and native ES6 + custom elements, see the comment above ^ |
@rniwa thanks for mentioning me but I'm not sure it's so easy. Babel is plain broken when it comes to super calls and my poly already patches 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. |
I've verified that both the ES6 and the Babel transpiled version works. The key here is to directly invoke |
I'll play with your implementation and see how it goes. Maybe it'll make ife easier for everyone in this way so ... why not. Thanks. |
@rniwa it takes just 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 |
Oh, I see, that's just broken. Babel needs to fix that. |
@rniwa just sent me to this issue. I'd like to share some of what we've done on the polyfill side of things... First, we have a "native shim" to the Custom Elements polyfill so that ES5 constructors can be used to implement elements. There have been two versions of this shim: The first version patched The new version patches up the CustomElementRegistry API to generate a stand-in class at There are some caveats that I list in the comments of the shim:
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. |
Because I've proposed that already in the related Babel bug (since Babel is bugged for this and every other native constructor call) and they told me they didn't want to lose performance. It looks like they delegated to you their transformation problem I've already said how to solve. Thanks for sharing anyway, but I'm not sure this is the right way to go. |
First, ES6 classes have a ugly static limitations (permanently engrained function BarBar() { HTMLElement.call(this); console.log('hello'); }
BarBar.prototype = Object.create(HTMLElement.prototype)
customElements.define('bar-bar', BarBar)
document.createElement('bar-bar') Output:
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:
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:
Honestly, why? |
Why is the web becoming inflexible? Why are we blocking the dynamic nature of pre-ES6? |
This is not about Web becoming inflexible. This is about using I've made a number of suggestions to solve this problem, one of which was about passing the local name from |
I feel like it may be a bad design for the
Definitely true, that would be ugly! If I understand correctly, Maybe we can add something to JavaScript? What if we add a new method to functions similar to function Foo(...args) {
HTMLElement.construct(this, args) // or similar, and new.target in HTMLElement is Foo.
}
Foo.prototype = Object.create(HTMLElement.prototype)
customElements.define('x-foo', Foo)
new Foo |
Aha!! I got it to work with ES5 classes using 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 |
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 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
@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. |
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!).
@morewry great point! |
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 The reason I thought it worked in other browsers besides Electron was because I was either writing markup, or using Would it be possible for the engine to special-case |
@trusktr const ES5Element = function() {}
customElements.whenDefined('es5-element').then(function() {
Reflect.construct(HTMLElement, [], ES5Element) // will never depend upon order
});
customElements.define('es5-element', ES5Element) |
FWIW, WebKit / Safari generates a slightly more helpful error message: |
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 |
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 |
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
|
Why? MutationObserver lets you observe when child elements are inserted or removed. That's exactly when you should be running the logic to update your container element. In general, the idea of all children being ready is flawed since an element can be inserted or removed dynamically. |
I got your point of element insertion or removal but there is high probability the MutationObserver runs multiple times if there are multiple children that are active. Anyways I don't think need for lifecycle event after children being ready is a flaw. Even many major frameworks react (componentDidMount), vue (mounted), angular (afterView/afterContent) provides you similar functionality. |
componentDidMount is the same as connectedCallback right? it has no concept of completeness of rendering. Consider the following example:
It seems only you as a component author can make sure you are done done... and you can use whatever means necessary to do so (Promise, Callback, Event, ...) PS: even more "crazy" example... assuming we take the finished loading of translations as being ready: an element that loads a form from an api which has some translations but also special input elements which need to load their own translations => so you could end up with the "form" being already ready (e.g. translations loaded) but the child elements are not (e.g. translations are not yet loaded) ... so to be truly ready you would need to check for every child elements readiness as well... => we are going down the rabbits hole 🙈 |
I haven't seen how react works internally much 😁 . I just tried logging Regarding example, true that it is getting complicated. I think that's where frameworks like react or vue won the game over custom elements/webcomponents. |
@thecodejack @daKmoR I really recommend to carefully study the whole spec to become familiar with all the caveats involved: |
@AndyOGo thx for clearing that up... so for the rendering However, the example with the loading of translations (via fetch to an external api server) is still valid right? or will |
@daKmoR Regarding loading of translations, yes it's still valid. I really like this interactive diagram for React Lifecycle hooks and would wish similiar for custom elements: |
what's wrong with that?
|
@xgqfrms that's not a valid custom element name |
@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); |
@domenic @morewry I just want to pop in and say that this is ridiculous. Javascript is a prototype-based language, and to get "consensus" it was necessary to block a critical feature from being possible using the prototype paradigm--and you refuse to fix it when it causes problems because of some sort of politics? It's nice that we have the option to use nice ES6 class syntax... but it's less powerful than raw prototyping and it's simply mind-boggling to me that my search for a solution led me to "there is none, because a committee banned it to force you to code their way even though the core features you're using aren't going anywhere". Sometimes features are deprecated and things aren't backwards compatible; sometimes poor design introduces obstacles; but "sometimes a committee wants everyone to code a certain way for no particular reason and introduces obstacles to using perfectly supported features in a given context on purpose"? Is Javascript deprecated? Or is there a plan to eliminate prototypes from the language and move to a purely class-based approach--then perhaps add in strong typing and C++ template syntax? If so, that's crazy; if not, then the lack of support for prototype-based custom components is a serious bug. Is there no way this can be brought up and addressed now that 4 years have passed? If the only reason there's no solution is that the design was intentionally crippled by bureaucratic fiat at a face-to-face meeting half a decade ago, perhaps it could be fixed now? I guess I will now proceed to implement a bizarre, unreadable, inefficient workaround or introduce some hideous class definitions into my otherwise class-free library, for no reason whatsoever except that "the owners of the internet don't like prototypes". |
@sapphous Class syntax is itself part of the “prototype paradigm,” but as others have mentioned, you don’t actually have to use it: function NoClassSyntaxElement() {
return Reflect.construct(HTMLElement, [], new.target);
// or Reflect.construct(HTMLElement, [], NoClassSyntaxElement), I suppose, if you don’t care about subclassing
}
NoClassSyntaxElement.__proto__ = HTMLElement;
NoClassSyntaxElement.prototype.__proto__ = HTMLElement.prototype;
customElements.define('no-class-syntax', NoClassSyntaxElement);
console.log(new NoClassSyntaxElement().matches(':defined')); // true This is useful if you need the super reference to be static but need to leave [[IsExtensible]] true (i.e., mimicking the behavior of platform interface constructors that inherit from others). That is pretty niche, mainly of interest for high-fidelity polyfilling stuff, but if for whatever reason you are averse to using Class syntax 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...
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. |
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 yieldsUncaught 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?
The text was updated successfully, but these errors were encountered: