babel: Native extends breaks HTMLELement, Array, and others
Current Babel transform, when it comes to call the parent
function _possibleConstructorReturn(self, call) {
if (!self) {
throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
}
return call && (typeof call === "object" || typeof call === "function") ? call : self;
}
It’s a way too poor implementation.
If we take the current basic ES6 compat syntax:
class List extends Array {}
We’ll realize babel does a bad job.
var l = new List();
l instanceof List; // false
The reason is simple: Babel replace the returns and exit, without caring about userland expectations.
This is how above basic extend should desugar:
function List() {
return Object.setPrototypeOf(
Array.apply(this, arguments),
List.prototype
);
}
It’s a very ad-hoc case for the initial example that currently fails, but it’s good enough to understand that inheriting the prototype is the least of the problem.
Indeed, we have 3 ways to do that within a transpiled code:
// losing the initial prototype
// for polyfilled ES5+ or lower
List.prototype = Object.create(
Array.prototype,
{constructor: {
configurable: true,
writable: true,
value: List.prototype
}}
);
// using a cleaner way
// for ES5+ compliant targets
Object.setPrototypeOf(
List.prototype,
Array.prototype
);
// using a cleaner way
// that works well with
// partially polyfilled .setPrototypeOf
List.prototype = Object.setPrototypeOf(
List.prototype,
Array.prototype
);
Solved the inheritance bit, considering Babel also set the prototype of each constructor,
we need to address cases where a super
call might “upgrade” the current context, like it is for HTMLELement
or exotic native objects.
// native ES6 static example
class List extends Array {
constructor(a, b, c) {
super(a, b);
this.push(c);
}
}
Above case should desugar to something like the follwoing:
function List(a, b, c) {
// the super bit
var self = Object.setPrototypeOf(
Array.call(this, a, b),
List.prototype
);
// the rest with swapped context
self.push(c);
// at the end
return self;
}
which is also ad-hoc example code for the previous example.
Considering a transpiler will get the arguments part easily right, this is how previous case could be generically transpiled for arguments used in both constructors:
// to make it as generic as possible
function Child() {
// the super call bit desugar to ...
var
instance = Object.getPrototypeOf(Child)
.apply(this, arguments),
type = instance ? typeof instance : '',
// if Parent overwrote its default return
self = (type === 'object' || type === 'function') ?
// upgrade the instance to reflect Child constructor
Object.setPrototypeOf(instance, Child.prototype) :
// otherwise use the current context as is
this
;
// at this point, the rest of the constructor
// should use `self` instead of `this`
self.push(c);
// and return `self` reference at the end
return self;
}
The last problem is that modern syntax would use Reflect to create any sort of object, instead of old, ES3 friendly, .call
or .apply
way.
Following a past, present, and even future proof approach:
var
reConstruct = typeof Reflect === 'object' ?
Reflect.construct :
function (Parent, args, Child) {
return Parent.apply(this, args);
}
;
function Child() {
// the super call bit
var
instance = reConstruct.call(
this,
Object.getPrototypeOf(Child),
arguments,
Child
),
type = instance ? typeof instance : '',
self = (type === 'object' || type === 'function') ?
Object.setPrototypeOf(instance, Child.prototype) :
this
;
// the rest of the constructor body
self.push(c);
// at the end or instead of empty returns
return self;
}
Above solution would work with userland, exotic, or DOM constructors, and in both native and transpiled engines.
About this issue
- Original URL
- State: closed
- Created 8 years ago
- Reactions: 18
- Comments: 40 (10 by maintainers)
Commits related to this issue
- Try with a different polyfill Due to documented problems of transpiled ES2015 classes and extending native constructors (babel/babel/issues/4480), I had to use the V0 api, it all works fine in develo... — committed to m-mujica/bitovi-projects-grid by deleted user 8 years ago
- refactored internals for readability seems worth it to avoid grawlix I'd use uitil's `CustomTag` abstraction, but Babel balks at that: https://github.com/babel/babel/issues/4480 — committed to innoq/simplete.obsolete by FND 7 years ago
- refactored internals for readability seems worth it to avoid grawlix I'd use uitil's `CustomTag` abstraction, but Babel balks at that: https://github.com/babel/babel/issues/4480 — committed to innoq/simplete by FND 7 years ago
- bump minimum browser versions to support Reflect.construct There is basically no way to do Custom Elements v1 with transpiled classes that doesn't involve some modern JS (either native classes or Ref... — committed to mixpanel/panel by tdumitrescu 7 years ago
- fixed https://github.com/babel/babel/issues/4480#issuecomment-306501841 — committed to WebReflection/fix-babel-class by WebReflection 7 years ago
@smalluban yes, it’s related, and Custom Elements extends are indeed the reason I came here.
Right … so, I’ve done some test and played around with the AST explorer. I’d like to announce here my new, and first ever, babel-plugin-transform-builtin-classes transformer.
It is based on the following
HELPER
:And it uses
.babelrc
to understand which native/global class you’d like to extend.The repository provides also a default one you could copy and paste around: https://github.com/WebReflection/babel-plugin-transform-builtin-classes/blob/master/.babelrc
It has all the native classes exported in the global window in Chrome, but you could use just few of them.
Everything else will be untouched.
You might want to use this together with the
preset-es2015
or bring in at least the classes related bit.If you’d like to have this mainstream/by default please let me know what should I do, thanks.
To whom this might concern, I’ve published a 40 LOC module that patches only classes that needs to be patched at distance.
https://github.com/WebReflection/fix-babel-class
Whenever you want to fix the issue you invoke
fixBabelClass(YourClassThatExtendsANativeOne)
.That’s pretty much it.
I am not sure if this is directly connected to this issue, but Babel class transpilation breaks using native
customElements
API. HTMLElement constructor has to be called withnew
operator, so in transpiled codeReflect
has to be used.I have found an article The JS super (and transpilers) problem with possible solution (last code example).
I created two codepen examples.
Babel transpiled class (http://codepen.io/smalluban/pen/wzgoON?editors=0012):
"Failed to construct 'HTMLElement': Please use the 'new' operator, this DOM object constructor cannot be called as a function."
"created element"
"created element"
Firefox/IE uses customElements polyfill, so it works well - there is no problem with
target.new
, but Chrome requires that.Custom transpiled class (http://codepen.io/smalluban/pen/JREbaW?editors=0012):
"created element"
"created element"
"created element"
In this example if
Reflect
API is present, it’s used to call parent constructor, and then creating custom element works in both native and polyfilled versions.I am not sure how this will work with “normal” class definition where special behavior is not required.
FYI the plugin has been updated after a fix related to N level inheritance. There are also tests now to validate it works on NodeJS. https://github.com/WebReflection/babel-plugin-transform-builtin-classes
Custom Elements also seem to work without any issue with or without my polyfill.
I believe all my tests are using the
Reflect.construct
available in all targets I am checking, but if you could confirm it’s OK even on older browsers/engines that’d be awesome.Best Regards
@zloirock So is there a fix coming for this? It basically makes web components / custom elements unusable. 😢
It looks like we all agree on this point: https://github.com/w3c/webcomponents/issues/587#issuecomment-254348659
Not even browser vendors developers can workaround the current broken state when it comes to
super
.I won’t have time soon to solve this issue, I’d like to understand if it has any priority though so at least I can better reply to people asking me why my polyfill is broken ( when the issue is them using Babel 😦 )
Thanks for any sort of ETA / outcome
Thanks everyone for making this happen - and of course @WebReflection in particular for figuring this out in the first place and staying on the ball.
For users who aren’t familiar with Babel’s internals, AFAICT the fix in #7020 will be included by default with the upcoming Babel v7.0? (If you’re using something like @babel/preset-es2015 or @babel/preset-env, that is - at least those presets seem to depend on @babel/plugin-transform-classes.)
We should at least add a link to the
babel-plugin-transform-classes
which suggests the users your plugin. Anyway, if you are ok with it, I can work on merging the two plugins.@zloirock have you seen my proposed changes ? https://github.com/babel/babel/issues/4480#issuecomment-256619501
You can compare them directly with the current output generated by
Current
Improved
The only overhead for non native constructors is during class definition time (so once per Application lifecycle) through:
Hopefully, that’s practically irrelevant for real-world projects.
@stephanbureau remember in V1 there are changes so that you need to specify upfront attributes to listen to.
Hi,
As a workaround on my side, I use
github:WebReflection/document-register-element
as polyfill to import. Then, the only issue I still encountered isattributeChangedCallback
which is not called when changing attributes values, not perfect I know.connectedCallback
ordisconnectedCallback
methods are correctly called though.For info, I am using jspm 0.17.0-beta.31 with babel 6.16.0.
Here’s an example with a basic class:
in ES6
Transpilled with babel runtime
Hope it helps a bit.