FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

Update A-Frame to custom elements v1 API by dmarcos · Pull Request #5136 · aframevr/aframe · GitHub

Update A-Frame to custom elements v1 API - #5136

Merged
dmarcos merged 1 commit into
aframevr:masterfrom
dmarcos:customElementsV1+
Oct 25, 2022
Merged

Update A-Frame to custom elements v1 API #5136
dmarcos merged 1 commit into
aframevr:masterfrom
dmarcos:customElementsV1+

Conversation

dmarcos commented Oct 13, 2022
edited
Loading

Copy link
Copy Markdown
Member

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 until DOMContentLoaded 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).

dmarcos changed the title Updating A-Frame to custom elements v1 API Update A-Frame to custom elements v1 API Oct 13, 2022

arpu commented Oct 13, 2022

Copy link
Copy Markdown
Contributor

better use https://github.com/ungap/custom-elements#readme ?

Warning: [@ungap/custom-elements](https://github.com/ungap/custom-elements#readme) is what you are looking for!
on https://www.npmjs.com/package/@webreflection/custom-elements

Comment thread src/core/a-mixin.js Outdated
}

doConnectedCallback () {
ANode.prototype.connectedCallback.call(this);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

what's wrong with:

super.connectedCallback();

? 🤔

Comment thread src/index.js Outdated
// Custom Elements v1 polyfill
if (!window.customElements) {
const customElementsPolyfill = require('@webreflection/custom-elements');
customElementsPolyfill(window || global);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

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.

Comment thread tests/node/test.js
global.document = _window.document;
global.screen = {};
global.HTMLElement = _window.Element;
global.window.customElements = {define: function () {}};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

FYI LinkeDOM has customElements too

dmarcos commented Oct 14, 2022

Copy link
Copy Markdown
Member Author

@kylebakerio Does this PR fix the issue with the camera?

Comment thread src/core/a-entity.js
}

this.addToParent();
AEntity.prototype.doConnectedCallback.call(this);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

We can use simply this.doConnectedCallback() here right?

Comment thread src/core/a-entity.js Outdated
if (this.isScene) { return; }
doConnectedCallback () {
var assetsEl; // Asset management system element.
var sceneEl = this.sceneEl;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Does this thing work?
From my understanding, this.sceneEl is defined when executing ANode connectedCallback() but you call it after here.

vincentfretin Oct 16, 2022
edited
Loading

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

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.

Comment thread src/core/a-entity.js
connectedCallback () {
// Defer if DOM is not ready.
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', this.connectedCallback.bind(this));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Same here, you wanted to write this.doConnectedCallback.bind(this) right?

Comment thread src/core/a-assets.js Outdated
if (this.timeout) { clearTimeout(this.timeout); }
}
},
ANode.prototype.connectedCallback.call(this);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

simply super.connectedCallback()

Comment thread src/core/a-assets.js
connectedCallback () {
// Defer if DOM is not ready.
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', this.connectedCallback.bind(this));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

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?

dmarcos Oct 15, 2022
edited
Loading

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

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

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

evaluating the condition only happens once so perf-wise is negligible over more clear code

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

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 called
  • if (document.readyState === 'loading') evaluated to true
  • document.addEventListener('DOMContentLoaded', this.connectedCallback.bind(this)); callback registered
  • connectedCallback called because of the DOMContentLoaded event
  • if (document.readyState === 'loading') evaluated to false
  • doConnectedCallback called

in comparison, using doConnectedCallback as the callback:

  • connectedCallback called
  • if (document.readyState === 'loading') evaluated to true
  • document.addEventListener('DOMContentLoaded', this.doConnectedCallback.bind(this)); callback registered
  • doConnectedCallback called because of the DOMContentLoaded event

dmarcos Oct 19, 2022
edited
Loading

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

I created it for readability. I like small focused functions when possible

dmarcos Oct 19, 2022
edited
Loading

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

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.

Comment thread src/core/scene/a-scene.js
connectedCallback () {
// Defer if DOM is not ready.
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', this.connectedCallback.bind(this));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

use doConnectedCallback here

Comment thread src/core/scene/a-scene.js Outdated
disconnectedCallback () {
// Remove from scene index.
var sceneIndex = scenes.indexOf(this);
super.disconnectedCallback.call(this);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

simply super.disconnectedCallback();

Comment thread src/core/a-entity.js
return;
}
// ANode method.
super.connectedCallback();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

This line should really be at the top.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

I see you chose the first suggestion, great.

Comment thread src/core/a-entity.js Outdated
for (componentName in this.components) {
this.removeComponent(componentName, false);
}
super.disconnectedCallback();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

We didn't have this call before. We call it a second time below.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Thanks. I removed the duplicated call but don't see disconnectedCallback called twice. Can you double check or share steps to reproduce? Thanks

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

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.

Comment thread src/core/a-entity.js Outdated
}
},
this.removeFromParent();
super.disconnectedCallback.call(this);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

simply super.disconnectedCallback();

Comment thread src/core/a-node.js
return element;
});
observer.observe(this, observerConfig);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

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?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

if you check WebKit source code around MO you realize having one or many doesn't make much of a difference.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Thanks @WebReflection for checking that. So we're good then.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

thanks @vincentfretin and @WebReflection for the feedback

Copy link
Copy Markdown
Contributor

@kylebakerio Does this PR fix the issue with the camera?

Tested it. seems it does.

Comment thread src/core/scene/a-scene.js
this.setAttribute('screenshot', '');
this.setAttribute('vr-mode-ui', '');
this.setAttribute('device-orientation-permission-ui', '');
super.connectedCallback();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

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.

dmarcos commented Oct 17, 2022

Copy link
Copy Markdown
Member Author

@kylebakerio Does this PR fix the issue with the camera?

Tested it. seems it does.

Thanks so much for taking the time to test it. Much appreciated.

Comment thread src/index.js
window.Promise = window.Promise || require('promise-polyfill');

require('@ungap/custom-elements');

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

@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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

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

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Thanks @WebReflection. Just to double check. We're good just requiring the polyfill, are we? No other checks or initialization necessary?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

P.S. if you've fully switched to customElements without builtin extends support, that might even not be needed

Copy link
Copy Markdown
Contributor

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

Comment thread src/core/a-node.js Outdated
return;
}
ANode.prototype.doConnectedCallback.call(this);
this.doConnectedCallback();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

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.

Copy link
Copy Markdown
Contributor

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.
Currently it's working great on Firefox and Chrome on Ubuntu, Chrome Android, Safari iPad where I'm testing usually.

dmarcos commented Oct 24, 2022

Copy link
Copy Markdown
Member Author

I think this is close to ready. If you give me your blessing I'll go ahead and merge.

Copy link
Copy Markdown
Contributor

There is just one last confusing code here https://github.com/aframevr/aframe/pull/5136/files#r1003525392 otherwise good to merge for me.

dmarcos commented Oct 25, 2022

Copy link
Copy Markdown
Member Author

All right! Merging. This PR is a scary one 😱 🤞 Perfect for Halloween. Thanks everybody for the feedback. Much much appreciated.

This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants


Back | FazBrowse Home | New Git URL