-
-
Notifications
You must be signed in to change notification settings - Fork 4k
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
Update A-Frame to custom elements v1 API #5136
Conversation
better use https://github.com/ungap/custom-elements#readme ?
|
src/core/a-mixin.js
Outdated
} | ||
|
||
doConnectedCallback () { | ||
ANode.prototype.connectedCallback.call(this); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what's wrong with:
super.connectedCallback();
? 🤔
src/index.js
Outdated
// Custom Elements v1 polyfill | ||
if (!window.customElements) { | ||
const customElementsPolyfill = require('@webreflection/custom-elements'); | ||
customElementsPolyfill(window || global); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you check for !window.customElements
without checking if window
exists at all, so this will never pass through the ||
, or better, code will fail anyway if window
is not around. I suggest to either remove that || global
, as never reached, and maybe use self
instead of window for Worker cases, or even better: globalThis
.
That being said, you could use the proper, maintained, polyfill instead.
@@ -11,6 +11,8 @@ suite('node acceptance tests', function () { | |||
global.navigator = _window.navigator; | |||
global.document = _window.document; | |||
global.screen = {}; | |||
global.HTMLElement = _window.Element; | |||
global.window.customElements = {define: function () {}}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FYI LinkeDOM has customElements too
@kylebakerio Does this PR fix the issue with the camera? |
|
||
this.addToParent(); | ||
AEntity.prototype.doConnectedCallback.call(this); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can use simply this.doConnectedCallback()
here right?
src/core/a-entity.js
Outdated
if (this.isScene) { return; } | ||
doConnectedCallback () { | ||
var assetsEl; // Asset management system element. | ||
var sceneEl = this.sceneEl; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this thing work?
From my understanding, this.sceneEl
is defined when executing ANode
connectedCallback()
but you call it after here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I confirm, I just added a console.log, this.sceneEl
is always undefined here. If you put super.connectedCallback();
above, it will contain the scene element.
connectedCallback () { | ||
// Defer if DOM is not ready. | ||
if (document.readyState === 'loading') { | ||
document.addEventListener('DOMContentLoaded', this.connectedCallback.bind(this)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here, you wanted to write this.doConnectedCallback.bind(this)
right?
src/core/a-assets.js
Outdated
if (this.timeout) { clearTimeout(this.timeout); } | ||
} | ||
}, | ||
ANode.prototype.connectedCallback.call(this); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
simply super.connectedCallback()
connectedCallback () { | ||
// Defer if DOM is not ready. | ||
if (document.readyState === 'loading') { | ||
document.addEventListener('DOMContentLoaded', this.connectedCallback.bind(this)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't understand why you created a doConnectedCallback
function if you don't use it here. The second time connectedCallback
is called, we know the condition is false, so you intended do use this.doConnectedCallback.bind(this)
here to not evaluate the condition again, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's effectively the same but this way when read it's more clear to see that we're just deferring the call. Don't have to read understand doConnectedCallback
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
evaluating the condition only happens once so perf-wise is negligible over more clear code
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In your logic, if it's more clear to you to see that we're just deferring the call, and I can agree with that, then you don't even need to create a separate function doConnectedCallback
, you can just put the code inside connectedCallback
after the condition, unless I'm missing something why you created it. That's why it bothered me reading this code, when I have this pattern I usually create a function like this to use it as the callback, mostly important when the condition is costly so it's not executed a second time. Here I agree the condition cost is negligible.
To me, the condition is evaluated twice when the readyState is loading:
connectedCallback
calledif (document.readyState === 'loading')
evaluated to truedocument.addEventListener('DOMContentLoaded', this.connectedCallback.bind(this));
callback registeredconnectedCallback
called because of the DOMContentLoaded eventif (document.readyState === 'loading')
evaluated to falsedoConnectedCallback
called
in comparison, using doConnectedCallback
as the callback:
connectedCallback
calledif (document.readyState === 'loading')
evaluated to truedocument.addEventListener('DOMContentLoaded', this.doConnectedCallback.bind(this));
callback registereddoConnectedCallback
called because of the DOMContentLoaded event
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I created it for readability. I like small focused functions when possible
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
even if evaluated twice cost is negligible and it happens before the scene loads. Dynamically added entities once the DOM has loaded won't defer the call.
connectedCallback () { | ||
// Defer if DOM is not ready. | ||
if (document.readyState === 'loading') { | ||
document.addEventListener('DOMContentLoaded', this.connectedCallback.bind(this)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
use doConnectedCallback here
src/core/scene/a-scene.js
Outdated
disconnectedCallback () { | ||
// Remove from scene index. | ||
var sceneIndex = scenes.indexOf(this); | ||
super.disconnectedCallback.call(this); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
simply super.disconnectedCallback();
return; | ||
} | ||
// ANode method. | ||
super.connectedCallback(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This line should really be at the top.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@dmarcos there is actually no issue here, but a rather confusing code. I actually didn't see you do this.sceneEl
twice here. You do this here:
var sceneEl = this.sceneEl; // but we know it's actually undefined because we didn't call super.connectedCallback()
...
super.connectedCallback();
...
sceneEl = this.sceneEl;
so I'd suggest just to use
var sceneEl;
...
super.connectedCallback();
...
sceneEl = this.sceneEl;
or
super.connectedCallback();
var sceneEl = this.sceneEl;
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see you chose the first suggestion, great.
src/core/a-entity.js
Outdated
for (componentName in this.components) { | ||
this.removeComponent(componentName, false); | ||
} | ||
super.disconnectedCallback(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We didn't have this call before. We call it a second time below.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tested the previous code, putting a console.log in detachedCallback
and removing an a-entity element via the chrome inspector, it was called twice, I'm not sure why twice.
Here in the new code, this is called four times. Removing this line, disconnectedCallback
is called twice, no error, tests are all passing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks. I removed the duplicated call but don't see disconnectedCallback
called twice. Can you double check or share steps to reproduce? Thanks
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can add a console.log exactly where you just removed the line, open the inspector, right click on the node and remove. Anyway it's the same behavior than in the previous version now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah I just understood why I have it twice, I was removing a-entity that has a child a-camera, so I'm removing two entities at the same time that's why.
src/core/a-entity.js
Outdated
} | ||
}, | ||
this.removeFromParent(); | ||
super.disconnectedCallback.call(this); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
simply super.disconnectedCallback();
return element; | ||
}); | ||
observer.observe(this, observerConfig); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is similar to what we had in the polyfill https://github.com/dmarcos/document-register-element/blob/cd665644367686d5645048f853d22fef9584bbf0/src/document-register-element.js#L529-L560 right? Previously we had a single MutationObserver instance for the whole document, here we have a MutationObserver instance for each node. Do we have to worry about performance here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not too familiar about how the old polyfill worked. Not sure about the overhead of an observer. Any ideas on how to do it with one are welcome. it's a bit unfortunate that customElements V1 observed attributes can only be declared statically.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not familiar with it either, it was actually the first time I saw a MutationObserver
object myself. Not sure about the performance really, I was just reading the comment about perf above the code I linked, this comment https://github.com/dmarcos/document-register-element/blob/cd665644367686d5645048f853d22fef9584bbf0/src/document-register-element.js#L526-L528
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if you check WebKit source code around MO you realize having one or many doesn't make much of a difference.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @WebReflection for checking that. So we're good then.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks @vincentfretin and @WebReflection for the feedback
Tested it. seems it does. |
this.setAttribute('screenshot', ''); | ||
this.setAttribute('vr-mode-ui', ''); | ||
this.setAttribute('device-orientation-permission-ui', ''); | ||
super.connectedCallback(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that in previous code, the super method was called before setting the default components. This super method should be called at the top, an example super.connectedCallback();
is initializing (currently wrongly) this.sceneEl
, and the component init may use this.sceneEl
but it's probably not the case here.
Thanks so much for taking the time to test it. Much appreciated. |
@@ -1,6 +1,8 @@ | |||
// Polyfill `Promise`. | |||
window.Promise = window.Promise || require('promise-polyfill'); | |||
|
|||
require('@ungap/custom-elements'); | |||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@WebReflection with this statement the polyfill is a pass-through if custom elements API is defined? Just following the how-to: https://github.com/ungap/custom-elements#how-to
Thanks for the feedback
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the poly includes features detection and leave untouched browsers that implement latest status of the specs:
- custom elements builtin extends (only safari / webkit gets fixed)
customElements.whenDefined('some-comp').then(Class => { ... })
is patched for older browsers that don't pass the related class to the promise once the definition is registered
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @WebReflection. Just to double check. We're good just requiring the polyfill, are we? No other checks or initialization necessary?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the poly includes features detection for all cases, including jurassic one where customElements is not a thing ... if you want to rise your target browsers, the Safari/WebKit only is an option but it requires manual feature detection ... happy to help with that though
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P.S. if you've fully switched to customElements without builtin extends support, that might even not be needed
Two important issues I commented that are not about code style #5136 (comment) and #5136 (comment) are not addressed yet, see my proposed changes here dmarcos#6 |
src/core/a-node.js
Outdated
@@ -39,7 +39,7 @@ class ANode extends HTMLElement { | |||
document.addEventListener('DOMContentLoaded', this.connectedCallback.bind(this)); | |||
return; | |||
} | |||
ANode.prototype.doConnectedCallback.call(this); | |||
this.doConnectedCallback(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This change was wrong, we need to put back the previous line.
I get this error with the change:
a-entity.js:73 Uncaught RangeError: Maximum call stack size exceeded
at a.doConnectedCallback (a-entity.js:73:3)
at a.connectedCallback (a-node.js:42:10)
at a.doConnectedCallback (a-entity.js:75:11)
at a.connectedCallback (a-node.js:42:10)
at a.doConnectedCallback (a-entity.js:75:11)
at a.connectedCallback (a-node.js:42:10)
at a.doConnectedCallback (a-entity.js:75:11)
at a.connectedCallback (a-node.js:42:10)
at a.doConnectedCallback (a-entity.js:75:11)
at a.connectedCallback (a-node.js:42:10)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For a-scene, a-assets, a-mixin, we can use this.doConnectedCallback();
because we don't inherit from those.
But for a-node and a-entity, you can't use it, because it will call the doConnectedCallback
from the derived class, we don't want that here.
I'm currently using a build with my changes in my project. I'll let you know if I find any other issue while I'm developing on it. |
0c84b75
to
cbaf647
Compare
I think this is close to ready. If you give me your blessing I'll go ahead and merge. |
There is just one last confusing code here https://github.com/aframevr/aframe/pull/5136/files#r1003525392 otherwise good to merge for me. |
All right! Merging. This PR is a scary one 😱 🤞 Perfect for Halloween. Thanks everybody for the feedback. Much much appreciated. |
4cd6374
to
021f972
Compare
A-Frame is still using custom elements v0 but always running through polyfill since the old API has been deprecated.
This modernizes the code base and makes use of the native APIs shipped in browsers today. Polyfill only kicks in for browser without an implementation of custom elements v1 API.
This will help us to better reason about #5124 and address the root cause.
This PR unifies behavior of all A-Frame defined HTML elements. All
connectedCallback
associated logic is deferred untilDOMContentLoaded
fires. This should eliminate the problems with entity inter-dependencies (e.g I want to query an entity from the init method of a component or system that might not have yet been initialized).