Closed rektide closed 8 years ago
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 Reflect.construct
to call HTMLElement
's constructor. e.g.
function CustomElement() {
return Reflect.construct(HTMLElement, [], CustomElement);
}
Object.setPrototypeOf(CustomElement.prototype, HTMLElement.prototype);
Object.setPrototypeOf(CustomElement, HTMLElement);
customElements.define('custom-element', CustomElement);
(Apologies if this issue has already been discussed elsewhere; I had entirely failed to consider it before and I haven’t seen it mentioned…)
Will this cause problems for existing JS codebases that use a WebComponents polyfill with a transpiler like Babel? For example, transpiling this code using Babel’s es2015
preset fails to work because the resulting JS doesn’t use Reflect.construct
:
class TestElement extends HTMLElement {
constructor () {
console.log('Constructin’');
super();
}
connectedCallback () {
console.log('Connectin’');
}
disconnectedCallback () {
console.log('Disconnectin’');
}
}
customElements.define('test-element', TestElement);
const testInstance = document.createElement('test-element');
document.body.appendChild(testInstance);
I understand that native custom elements won’t be available in browsers that don’t already support ES-2015 class syntax, but if someone is using Babel + a polyfill for web components, it seems like they’d have a situation where their code works in older browsers (because the polyfill is active), but not in newer ones (because the polyfill just defers to the native implementation). That seems like a pretty big practical problem, but is it one you are concerned about here?
It is true that if you're using Babel and polyfill, then the above code won't work out-of-box but that's true of any polyfill that got written before the standard is finalized.
There are various ways to workaround such issues, and probably the simplest solution is to wrap the thing you pass to customElements.define
with a class. e.g.
function defineCustomElementInBabel(name, legacyConstructor) {
var wrapperClass = class extends legacyConstructor {
constructor() {
var newElement = new Reflect.construct(HTMLElement, [], wrapperClass);
legacyConstructor.call(newElement);
return newElement;
}
};
customElements.define(name, wrapperClass);
}
Obviously, this leaves new TestElement
non-functional. An alternative approach is to replace super()
call in TestElement
by something special like:
class TestElement extends HTMLElement {
constructor () {
constructCustomElement(TestElement);
}
with
function constructCustomElement(newTarget) {
Reflect.construct(HTMLElement, [], newTarget);
}
There are dozens of other ways to cope with this limitations and that's really up to framework and library authors.
On a broader note, I don't think the standards process or API design in standards should be constrained by polyfills written before the general consensus on the API shape has been reached and at least two major browser engines have implemented it. Also, deploying a polyfill on production before the standards have become stable is almost always a bad idea.
It is true that if you're using Babel and polyfill, then the above code won't work out-of-box but that's true of any polyfill that got written before the standard is finalized.
I suppose I was really most focused here on the impact to existing codebases. It’s not as if that hasn’t been a consideration in other web standards, though I do understand that current usage of polyfills for custom elements (and especially v1-esque polyfills) is quite small.
On the other hand, there is a lot of Babel usage out there (the majority of non-trivial JS codebases I’ve worked on as a consultant over the past year have used it), and I hadn’t really expected that I’d need such an awkward and specialized method for creating a custom element with it. It may be further complicated in trying to find solutions that allow someone to inherit from a custom element provided as a third-party module, where the provider of the component may have solved the issue in their own way. As you noted, there are many ways to work around it.
Also, deploying a polyfill on production before the standards have become stable is almost always a bad idea.
I agree! I’ve just spent a lot of time shaking my head at bugs I’ve had to fix for clients because they shipped code that depends on an alpha/beta version of a library or a polyfill for a standard that hasn’t been finalized yet, so I’m sensitive to these kinds of decisions.
At the end of the day, I’m just a little frustrated at realizing the API for custom elements is less friendly than I had thought (again, entirely my fault for not reading as closely as I should have). I also understand that this is well past the point where anyone is willing to rethink it.
(I also want to be clear that I really appreciate the work being done here by everyone on the working group. Obviously I would have liked this issue to turn out differently, but I’m not complaining that this is some horrible travesty. The big picture is still an improvement for the web.)
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);
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);
@WebReflection: In the case, you're still looking for a solution that works in both Babel + Polyfill and native ES6 + custom elements, see the comment above ^
@rniwa thanks for mentioning me but I'm not sure it's so easy.
Babel is plain broken when it comes to super calls and my poly already patches HTMLELement
, so does the one from googlers.
I strongly believe this should be solved on Babel side, otherwise we're blocking and degrading native performance because of tooling on our way.
Tooling should improve and help, not be a problem.
I've verified that both the ES6 and the Babel transpiled version works. The key here is to directly invoke Reflect.construct
in your polyfill and not rely on Babel's super()
call which, as you pointed out, is broken.
I'll play with your implementation and see how it goes. Maybe it'll make ife easier for everyone in this way so ... why not.
Thanks.
@rniwa it takes just new MyElement();
to fail with an illegal constructor
error and the problem with babel is that even if you have that this._id
set during constructor invokation, any other method defined in the class won't be inherited so no, your one does not seem to be a solution.
To summarize the issue:
class List extends Array {
constructor() {
super();
this._id = 1;
}
method() {}
}
console.log((new List).method); // undefined
It doesn't matter if you have set something in the constructor if everything else is unusable
edit: in your case just add a method to your MyElement
class and try to use it, it won't be there
Oh, I see, that's just broken. Babel needs to fix that.
@rniwa just sent me to this issue. I'd like to share some of what we've done on the polyfill side of things...
First, we have a "native shim" to the Custom Elements polyfill so that ES5 constructors can be used to implement elements. There have been two versions of this shim:
The first version patched window.HTMLElement
as a constructor function that used Reflect.construct
along with this.constructor
to emulate new.target
. This has some prohibitive performance issues because 1) Reflect.construct
is slow and 2) Reflect.construct
isn't a real substitute for super()
as it always creates a new instance, so this new HTMLElement
constructor would always throw away the currently initializing instance and return a new Element instance. (old version: https://github.com/webcomponents/custom-elements/blob/b43236a7da0917ea938b6cb1aa3116caaeb6e151/src/native-shim.js )
The new version patches up the CustomElementRegistry API to generate a stand-in class at define()
time and define that, and then keep it's own registry of user-defined ES5 constructors. It then does some shuffling for initialization. This approach is much faster and incurs only a 10% overhead over native CEs. The new version is here: https://github.com/webcomponents/custom-elements/blob/master/src/native-shim.js
There are some caveats that I list in the comments of the shim:
return SuperClass.call(this)
this
reference should not be used before the emulated super() call just like this
is illegal to use before super() in ES6.1) is a restriction because ES5 constructors cannot emulate super()
and call into an ES6 constructor. 2) is just making ES5 constructors slightly more spec-compliant with ES6 constructors and required because HTMLElement sometimes returns an object other than this
. I've worked with the major compilers to get their class transformations to implement this properly. Babel already worked. TypeScript has just fixed this, and Closure's fix is in review now. 3) is just respected the TDZ for this
even in ES5 constructors. This shouldn't be something that authors need to care about if they write ES6. 4) is the same restriction that native CEs have.
What this means for Custom Elements authors is that everyone should write and distribute ES6 classes and let applications do any compiling down to ES5 that they need. This is a little different than the current norm of writing in ES6 and distributing ES5, but it will be necessary for any libraries that extend built-ins - Custom Elements aren't really unique here. Apps can either send ES5 to older browsers and ES6 to newer browser, or ES5 to everything using the shim.
Object.setPrototypeOf(this, elementProto)
per each custom elements is just 10% slower?
Because I've proposed that already in the related Babel bug (since Babel is bugged for this and every other native constructor call) and they told me they didn't want to lose performance.
It looks like they delegated to you their transformation problem I've already said how to solve.
Thanks for sharing anyway, but I'm not sure this is the right way to go.
First, ES6 classes have a ugly static limitations (permanently engrained super
references), and now we can't use ES5 classes in custom elements? What if we generate those classes from a class library? This is not ideal. The following should NOT give an error:
function BarBar() { HTMLElement.call(this); console.log('hello'); }
BarBar.prototype = Object.create(HTMLElement.prototype)
customElements.define('bar-bar', BarBar)
document.createElement('bar-bar')
Output:
Uncaught TypeError: Failed to construct 'HTMLElement': Please use the 'new' operator, this DOM object constructor cannot be called as a function.
and
function BarBar() { var _this = new HTMLElement(); console.log('hello'); return _this }
BarBar.prototype = Object.create(HTMLElement.prototype)
customElements.define('bar-bar', BarBar)
document.createElement('bar-bar')
output:
Uncaught TypeError: Illegal constructor
and
function BarBar() { var _this = new HTMLElement(); console.log('hello'); return _this; }
BarBar.prototype = Object.create(HTMLElement.prototype)
customElements.define('bar-bar', BarBar)
document.createElement('bar-bar')
output:
Uncaught TypeError: Illegal constructor
Honestly, why?
Why is the web becoming inflexible? Why are we blocking the dynamic nature of pre-ES6?
This is not about Web becoming inflexible. This is about using [NewTarget]
internal slot. new HTMLElement
doesn't work because localName
cannot be determined inside HTMLElement
's constructor.
I've made a number of suggestions to solve this problem, one of which was about passing the local name from createElement
, custom element's constructor, and then to HTMLElement
. In this world, we can do the reverse lookup from the local name to the constructor object, and construct the element. However, this approach allows an inconsistency between the the actual constructor of HTMLElement
's constructor and what HTMLElement
's constructor ends up creating. Furthermore, it requires the HTMLElement
's constructor to be called with the local name as an argument, which many people argued are unnecessary and ugly. Optionally allowing this would mean that the behavior of HTMLElement
's constructor would flip between two modes, which is also not ideal.
I feel like it may be a bad design for the localName
string property to be coupled to specific semantics of the JavaScript language. I like that you tried to fix the problem; it would allow the end user of the API to pass in any valid JavaScript class, not just ES6 classes and I think that would be very beneficial because not everyone wants to use ES6 classes all the time.
Furthermore, it requires the HTMLElement's constructor to be called with the local name as an argument, which many people argued are unnecessary and ugly.
Definitely true, that would be ugly!
If I understand correctly, new.target
doesn't work with ES5 classes because calling a super constructor in the form SuperConstructor.call(this)
means that there won't be a new.target
reference inside SuperConstructor
, so when HTMLElement
is used like that it won't have a new.target
and therefore cannot look up the constructor in the custom element registry?
Maybe we can add something to JavaScript? What if we add a new method to functions similar to Reflect.construct
and that takes a context object, and only works when the containing constructor is called with new
.
function Foo(...args) {
HTMLElement.construct(this, args) // or similar, and new.target in HTMLElement is Foo.
}
Foo.prototype = Object.create(HTMLElement.prototype)
customElements.define('x-foo', Foo)
new Foo
Aha!! I got it to work with ES5 classes using Reflect.construct
! Try this in console:
function Bar() {
console.log('Bar, new.target:', new.target)
let _ = Reflect.construct(HTMLElement, [], new.target)
_.punctuation = '!'
return _
}
Bar.prototype = Object.create(HTMLElement.prototype)
function Baz() {
console.log('Baz, new.target:', new.target)
let _ = Reflect.construct(Bar, [], new.target)
return _
}
Baz.prototype = Object.create(Bar.prototype)
Baz.prototype.sayHello = function() {
return `Hello ${this.localName}${this.punctuation}`
}
customElements.define('x-baz', Baz)
const baz = new Baz
console.log(baz.sayHello())
And this
inside the Baz.prototype.sayHello
is as expected! So, problem solved! I am HAPPY! One just has to use that Reflect.construct
pattern, and inside a constructor manipulate _
instead of this
, which I don't mind doing. A downside is that it creates a wasteful object on instantiation because this
isn't being used and then will get GCed after the constructor returns _
, so double the object creation.
If you can use Reflect.construct
, just do that. I’ve stated it in https://github.com/w3c/webcomponents/issues/587#issuecomment-254017839.
Please go read the discussions above before adding a new comment.
Oops, you're right, but also to note that I'm using new.target
which is important if the class hierarchy is deeper. Sorry! And Thanks!
Honestly all of these workarounds just make me not want to write web components. You're not doing a very good job at selling this technology. "Oh no, it would be inconvenient for the browser vendor to implement" is not a valid excuse.
@trusktr this kind of workaround must not exist in a standard, it's too messy. Also forcing the developers to use only es6 classes will throw a lot of potential web components adopters to other technologies and frameworks, turning the main objective of web components not reachable...
Again, you can just use Reflect.construct
to make it work with non-class constructors. See https://github.com/w3c/webcomponents/issues/587#issuecomment-254017839. In addition, Safari, Firefox, and Chrome all have been shipping with class syntax for more than a year.
@klarkc @Giwayume this look not messy -> https://github.com/WebReflection/classtrophobic/#classtrophobic-
@SerkanSipahi This is about the natural language. The idea of web components is that they're universal and you don't have to install a bunch of 3rd party dependencies a page to get them to "just work".
Each of us users now has to babel. (ex: https://stackoverflow.com/questions/43287186/why-is-wecomponentsjs-custom-elements-es5-adapter-js-not-working )
Why would you or someone not distribute the es5 version of the needed .js libraries, such as components and shadow so that part is done for us? We would then just need to babel the acctual component, not the dependencies done N times, one per each remote project that uses standard components.
@rniwa thanks for es5 extending example with Reflect.construct, I was already going to abandon idea to use webcomponents to refactor messy legacy vanillajs/jq code.
@rniwa Simply using Reflect.construct
doesn't solve problems.
Reflect.construct
is available only in Edge 12+! That's a big (bad) deal because we need to support older systems through polyfills that can't be polyfilled properly, and Custom Elements v1 is relying on not-completely-polyfillable requirements in a day and age when we need it to be 100% polyfillable to make everyone's lives easier.
The strict enforcement of using new
means that transpilers can not compile to ES5 without using Reflect.construct
because Reflect.construct
won't work in anything below Edge 12.
This is a bad because not everyone is off of IE 10 or 11 yet, and there's no way to transpile this to a format that doesn't use new.target
or Reflect.construct
.
What this means is that people transpiling this to ES5 and using a Custom Elements v1 and Reflect
polyfills will get lucky that it works in IE10, but then they'll get the following error in new browsers like Chrome:
Uncaught TypeError: Failed to construct 'HTMLElement': Please use the 'new' operator, this DOM object constructor cannot be called as a function
For example, see how bad this is with [Buble output](https://buble.surge.sh/#class%20FooBar%20extends%20HTMLElement%20%7B%0A%20%20constructor()%20%7B%0A%20%20%20%20super()%0A%20%20%20%20console.log('foo-bar')%0A%20%20%7D%0A%7D%0A%0AcustomElements.define('foo-bar'%2C%20FooBar)%0A%0Anew%20FooBar). Run the output code in Chrome.
This is not good! It makes things very difficult, especially in a time when many people still have to support non-native-ES2015+ environments.
Sorry for posting this twice, I wanted everyone to see it.
Buble is "batteries included", meaning I can't configure it to use Reflect.construct
only for certain envs like I probably can with babel-preset-env, which means I'd have to switch to Babel, then I'd have to start making multiple builds, which will get very ugly for library authoring.
Aha, I know what the best solution is (thought it's not that good):
I can tell app developers: if you want to register my Custom Elements, you need to detect the user's browser, then use document.registerElement
in IE (with polyfill), otherwise you can likely use customElements.define
in all the other browsers (including Edge) because they support Reflect.construct
.
Now, that's uuugly!
The reason I'm having this problem is because I'm writing classes that I can use with Custom Elements v1 using constructor
s, but I need to support older browsers that don't have Reflect.construct
, so I'm writing classes that work when registered with v0
or v1
(this seems like a reasonable thing for a Custom Element library author to want).
@trusktr this problem is why we created the custom-elements-es5-adapterjs, that I've mentioned here before: https://github.com/webcomponents/webcomponentsjs#custom-elements-es5-adapterjs
Wow Justin, that's an incredible hack (using Object.setPrototypeOf(this, ...)
inside the native constructor. I hadn't thought of such a thing before.
The arrow function, const
, and Map
is still in the way though. If we're running this code in older browsers (which is probably true if we're compiling to ES5), it will throw a syntax error.
Luckily for me, I'm supporting IE11+, so I only need to change to non-arrow.
The shim is intended to be run on browsers with native Custom Elements, to allow ES5 elements. On other browsers just use the polyfill.
To make things easy for people (f.e. people learning HTML and barely JavaScript), things just needs to work without complications. As such, I will include this adapter in my library so that things are simply easy.
(I'm not saying it's ideal from a technical standpoint of manageability and flexibility, just that in most cases it is easier to just ship polyfills for end users, especially if the polyfill does nothing if the thing being polyfilled already exists).
I'm not a fan of telling people "hey, you're running in IE, use this, this, this, this, this, and this polyfill". That can be somewhat annoying for people, and sometimes turns them away from simply trying something they otherwise might've liked.
It's like page load times: if the page takes too long to load, people may just leave a site that they might've otherwise liked; the same concept.
What will this do in older browsers that have a window.customElements
polyfill? Hmmm, also class syntax will trip IE11.
What's your recommendation for libs then? I don't want to tell people: "detect the browser on server side and send the script tag if needed" for example. I want to tell people, regardless of browser, "use this script tag for my library" and done.
In case this is helpful, we are detecting the browser and dynamically loading the right libraries based on the browser. We do it inside our library so our users don't have to see that.
But instead of rolling your own dynamic loading, you could instead use https://github.com/webcomponents/webcomponentsjs#webcomponents-loaderjs. This does dynamic loading for you.
Best, Jeff Robbins
On Sat, Sep 30, 2017 at 3:01 PM Joe Pea notifications@github.com wrote:
What will this do in older browsers that have a window.customElements polyfill? Hmmm, also class syntax will trip IE11.
What's your recommendation for libs then? I don't want to tell people: "detect browser on server side and send the script tag if needed" for example. I want to tell people, regardless of browser, "use this script tag for my library" and done.
— 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-333328992, or mute the thread https://github.com/notifications/unsubscribe-auth/ACAWh_k8NlA5nTzLyJyxZf7QbZgUq1rLks5sno_zgaJpZM4KXuh2 .
Here's the funkiness I used on a recent angular + custom elements project to conditionally load the custom-elements-adapter. angular-cli will use webpack to dynamically inject polyfills at the end of your index.html, so that's how I load webcomponents.js after this bit has had a chance to run. Frankly it's not awesome, but it may be something that we can put into a webpack plugin so devs don't have to muck with it much :\
@robdodson that's an interesting trick. I did something else: I changed arrows to functions, and put the class
definition inside an eval()
and it worked that way without causing syntax errors.
But I have another problem: Apparently this all works great with Babel's ES5 output, but I get errors with Buble's ES5 output (I'm using Buble to transpile my lib). I decided not to muck with it for now and support Edge 13+, and maybe by the time the lib I'm making is actually ready for anything serious this adapter won't be needed anymore! 😆
I'm not sure if this is true but you might check to see if eval'ing the class like that fails in a CSP environment.
On Sat, Sep 30, 2017, 10:27 PM Joe Pea notifications@github.com wrote:
@robdodson https://github.com/robdodson that's an interesting trick. I did something else: I changed arrows to functions, and put the class definition inside an eval() and it worked that way without causing syntax errors.
But I have another problem: Apparently this all works great with Babel's ES5 output, but I get errors with Buble's ES5 output (I'm using Buble to transpile my lib). I decided not to muck with it for now and support Edge 13+, and maybe by the time the lib I'm making is actually ready for anything serious this adapter won't be needed anymore! 😆
— You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub https://github.com/w3c/webcomponents/issues/587#issuecomment-333354673, or mute the thread https://github.com/notifications/unsubscribe-auth/ABBFDdr_smTZqmKu64nKbdcp4sNsuqZoks5snyK0gaJpZM4KXuh2 .
Wow Justin, that's an incredible hack (using Object.setPrototypeOf(this, ...) inside the native constructor. I hadn't thought of such a thing before.
you were worried about Edge 12+, setPrototypeOf can't be polyfilled in IE < 11.
Anyway, the issue has been addressed and solved for Babel ages ago, without needing extra files. https://github.com/WebReflection/babel-plugin-transform-builtin-classes
@robdodson
CSP environment
What's that?
I see, thanks. That'd be yet another complication to worry about. For my case, I've become content on supporting Edge 13 and up with native Classes, Proxies, and Reflect.construct to do some cool tricks.
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?