Skip to content
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

[Scoped Registries] Can disconnected elements can retain their shadowroot via dangling treeroot pointer? #1078

Closed
keithamus opened this issue Oct 15, 2024 · 11 comments

Comments

@keithamus
Copy link
Collaborator

keithamus commented Oct 15, 2024

In w3c/tpac2024-breakouts#26 we discussed the idea of how we could store the shadowroot of an element created with shadowroot.createElement. Storing the originating shadowroot of calls to this would make this APIs much more ergonomic, as calls to for e.g. innerHTML on that node could retain the scoped registry in order to correctly assign definitions to custom elements during that call. Without this these APIs take an ergonomic hit.

It was raised after the meeting (and therefore not in the minutes) that one way to do this without introducing overhead would be to utilise the dangling pointer for the "TreeRoot", which is a nullptr for disconnected nodes. In Chromium this pointer is to Node, nsINode in Gecko and EventTarget in WebKit. Each of these classes has a set of bitflags with what looks like 1 flag remaining (Chromium's NodeFlags, and WebKit's EventTargetFlag explicitly mention 1-bit free, and while Gecko's BooleanFlag uses 32 bits, the last bit is a guard value which could potentially be repurposed).

So - just to check in with implementers (and specifically @emilio, @mfreed7, @rniwa who were in the meeting and may recall the discussion, also /cc @smaug---- for more confirmation on the Gecko side) - could we always populate the TreeScope with a Node subtype or Node instance which has a new bit set to say "this is a dummy proxy Node that points to a parent but shouldn't be given to script, and is just for keeping the shadowroot reference alive so that createElement & co work seamlessly" (the variable name could use some bikeshedding).

@keithamus keithamus changed the title [Scoped Registries] Disconnected elements can retain their "origin" by utilising the dangling TreeScope ptr [Scoped Registries] Can disconnected elements can retain their shadowroot via dangling treeroot pointer? Oct 15, 2024
@mfreed7
Copy link

mfreed7 commented Oct 15, 2024

So - just to check in with implementers (and specifically @emilio, @mfreed7, @rniwa who were in the meeting and may recall the discussion, also /cc @smaug---- for more confirmation on the Gecko side) - could we always populate the TreeScope with a Node subtype or Node instance which has a new bit set to say "this is a dummy proxy Node that points to a parent but shouldn't be given to script, and is just for keeping the shadowroot reference alive so that createElement & co work seamlessly" (the variable name could use some bikeshedding).

I'm supportive of an approach like this to allow us to keep track of the link to follow to find the correct (scoped) registry. One nit might be that I was thinking you'd put a bit on Element that means "my TreeScope reference isn't real - I'm disconnected. But the TreeScope reference does point to a TreeScope that has a registry that you can use for element creation". That's maybe what you meant by that paragraph, but I just wanted to make sure. But that's really in the details - I'm supportive.

@keithamus
Copy link
Collaborator Author

Oh yeah 🤦 that makes much more sense!

@rniwa
Copy link
Collaborator

rniwa commented Nov 1, 2024

Could someone clarify the exact situation in which this will be useful again?

@justinfagnani
Copy link
Contributor

Could someone clarify the exact situation in which this will be useful again?

When you create an element via a scoped registry, but it's not attached to the document, ie:

const el = shadowRootWithRegistry.createElement('div');
el.innerHTML = `<x-foo></x-foo>`;
shadowRootWithRegistry.append(el);

We don't want <x-foo> upgrading in the gloabal registry in this case.

@rniwa
Copy link
Collaborator

rniwa commented Nov 1, 2024

But the element isn't gonna upgrade until it's connected anyway, and at the time of connection, the element belongs to the shadow root with the scoped custom element registry so the normal lookup would work.

@justinfagnani
Copy link
Contributor

If <x-foo> defined in the global registry it will upgrade immediately, right?

@rniwa
Copy link
Collaborator

rniwa commented Nov 2, 2024

No. Customer elements only upgrade when they're connected.

@keithamus
Copy link
Collaborator Author

@rniwa the constructor functions run though, right? And so instanceof checks pass. For example:

customElements.define('foo-bar', class extends HTMLElement {
  constructor() {
    super()
    console.log('FooBar element was constructed')
  }
})
let div = document.createElement('div')
div.innerHTML = '<foo-bar></foo-bar>'

I wrote up some WPTs for this here: web-platform-tests/wpt#46170

@rniwa
Copy link
Collaborator

rniwa commented Nov 8, 2024

@keithamus Hm... you're right. The constructor runs in this case.

@rniwa
Copy link
Collaborator

rniwa commented Nov 21, 2024

Okay, so it's clear that we want to use the scoped custom element registry in the case of setting innerHTML to an element in an element newly created for a scoped custom element registry:

e.g.

function createConnectedShadowTree(registry) {
    const host = document.createElement('div');
    const shadowRoot = host.attachShadow({mode: 'closed', registry});
    document.body.appendChild(host);
    return shadowRoot;
}

const registry = new CustomElementRegistry;

class SomeElement extends HTMLElement { };
registry.define('some-element', SomeElement);

class OtherElement extends HTMLElement { };
registry.define('other-element', OtherElement);

const shadowRoot = createConnectedShadowTree(registry);
const someElement = shadowRoot.createElement('some-element');
someElement.innerHTML = '<other-element></other-element>';
someElement.querySelector('other-element') instanceof OtherElement; // This should evaluate to true.

But what happens when such an element gets inserted into another shadow tree with a different scoped registry? Or what happens if the same element gets removed from the shadow tree?

class OtherElement1 extends HTMLElement { };
customElements.define('other-element', OtherElement1);

const registry1 = new CustomElementRegistry;
class SomeElement extends HTMLElement { };
registry1.define('some-element', SomeElement);
class OtherElement2 extends HTMLElement { };
registry1.define('other-element', OtherElement2);

const registry2 = new CustomElementRegistry;
class OtherElement3 extends HTMLElement { };
registry2.define('other-element', OtherElement3);

const shadowRoot1 = createConnectedShadowTree(registry1);
const shadowRoot2 = createConnectedShadowTree(registry2);
const someElement = shadowRoot1.createElement('some-element');
someElement.innerHTML = '<other-element></other-element>';
someElement.querySelector('other-element') instanceof OtherElement2; // This should evaluate to true.
shadowRoot2.appendChild(someElement);
someElement.innerHTML = '<other-element></other-element>';
someElement.querySelector('other-element') instanceof OtherElement1; // This should evaluate to true.
someElement.remove();
someElement.innerHTML = '<other-element></other-element>';
someElement.querySelector('other-element') instanceof OtherElement1; // Should this evaluate to true?

@rniwa
Copy link
Collaborator

rniwa commented Nov 21, 2024

Closing this as a duplicate of #1040.

@rniwa rniwa closed this as completed Nov 21, 2024
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

No branches or pull requests

4 participants