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

Is there a way to detect if a custom element was constructed during parsing? #789

Closed
trusktr opened this issue Jan 31, 2019 · 17 comments
Closed
Labels

Comments

@trusktr
Copy link
Contributor

trusktr commented Jan 31, 2019

F.e.,

class MyEl extends HTMLElement {
 constructor() {
  if (/* isParsing? */) {
    console.log('created during parsing')
  }
 }
 connectedCallback() {
  if (/* isParsing? */) {
    console.log('connected during parsing')
  }
 }
}
@trusktr trusktr changed the title Is there a way to detect is a custom element was constructed during parsing? Is there a way to detect if a custom element was constructed during parsing? Jan 31, 2019
@caridy
Copy link

caridy commented Jan 31, 2019

this.isConnected and this.parentNode seems to be enough signal for elements created from parsing a fragment, e.g., run this in the console:

class Foo extends HTMLElement {
    constructor() {
        super();
        console.log(this.isConnected, this.parentNode);
    }
}
customElements.define('x-foo', Foo);
> undefined
new Foo()
> VM298:4 false null
> <x-foo>​</x-foo>​
document.body.innerHTML = '<x-foo></x-foo>'
> VM298:4 true <body>​…​</body>​
> "<x-foo></x-foo>"

@rniwa rniwa added the question label Feb 1, 2019
@rniwa
Copy link
Collaborator

rniwa commented Feb 1, 2019

this.isConnected and this.parentNode seems to be enough signal for elements created from parsing a fragment, e.g., run this in the console
That is true of any element which got upgraded.

In general there is no way to know whether a given element is created by the parser or not. The only thing you can observe is whether an element got upgraded by getting inserted into a document or constructed synchronously.

I don't quite understand what the use case for such a detection is, however.

@trusktr
Copy link
Contributor Author

trusktr commented Feb 1, 2019

Thanks guys! Are there any edge cases that this.isConnected && this.parentNode won't catch?

How would you recommend polyfilling isConnected? Is the best way to traverse upward until we find a document? What about with shadow roots? Support for isConnected is currently low.

In my MutationObserver-based childConnected/DisconnectedCallback example I used my own isConnected, but it is set in connectedCallback.

@rniwa
Copy link
Collaborator

rniwa commented Feb 1, 2019

When a custom element had been upgraded, then this.isConnected && this.parentNode would also true. Also, when a custom element had been defined before the HTML parser (not fragment parser) parsed an element, then this.isConnected && this.parentNode would be false because the element is neither connected nor has a parent node in that case. Also, it's probably redundant to check this.parentNode because you can't have a connected tree without having a parent except Document itself. So I don't see how such a check could ever be correct.

In general, I'd be interested to know what the exact use case is for detecting when a parser is creating a custom element. Some builtin elements do this (e.g. form control, script, etc...) but mostly due to legacy behaviors that is needed for backwards compatibility)

Finally, the best way to polyfill isConnected is something like this:

if (!Object.getOwnPropertyDescriptor(Node.prototype, 'isConnected')) {
    let rootNode = null;
    if (Node.prototype.getRootNode)
        rootNode  = (node) => node.getRootNode({composed: true});
    else {
        rootNode = (node) => {
            for (let ancestor = node, ancestorParent; ancestor; ancestor = ancestorParent) {
                ancestorParent = ancestor.parentNode || ancestor.host;
                if (!ancestorParent)
                    return ancestor;
            };
            return node;
        }
    }
    Object.defineProperty(Node.prototype, 'isConnected', {
        get() { return rootNode(this).nodeType == Node.DOCUMENT_NODE; },
        enumerable: true,
        configurable: true,
    });
}

@rniwa
Copy link
Collaborator

rniwa commented Feb 1, 2019

You can add some kind of caching but then you'd have to invalidate whenever a node is removed / inserted so it's probably not a great trade off.

@trusktr
Copy link
Contributor Author

trusktr commented Feb 1, 2019

Ah, thanks! I suppose we'd also need to patch attachShadow for the "closed" shadow roots, to store their hosts.

I'd be interested to know what the exact use case is for detecting when a parser is creating a custom element.

I was wondering if it would be helpful in any way for improving the MutationObserver-based example of child connected/disconnected callbacks.

@rniwa
Copy link
Collaborator

rniwa commented Feb 1, 2019

No, there is no need to store any host because host is always available regardless of whether the shadow root is closed or open.

@trusktr
Copy link
Contributor Author

trusktr commented Feb 2, 2019

Ah, okay, thanks! For some reason I didn't think that was the case, but just verified that it works.

@trusktr
Copy link
Contributor Author

trusktr commented Mar 18, 2019

Is the "fragment parser" invoked when I do something like

document.body.insertAdjacentHTML('beforeend', '...')

?

The reason I'm asking is because this other form of parsing seems to work differently than initial-payload parsing, leading me to yet another case that I need to detect:

In parsing of the initial HTML payload markup,

  1. if custom elements definitions are defined before parsing (f.e. in a script tag in the head),
  2. then if a custom element creates a MutationObserver with childList: true in its constructor
  3. when the parser finally upgrades this custom element, a MutationObserver will be created before the element's children have been upgraded
  4. The MutationObserver will fire mutation reactions for childList after the custom element's children have been upgraded.

However, if I use document.body.insertAdjacentHTML('beforeend', '...') to append a custom element and its children (instead of having the custom element and its children in the initial HTML payload's markup) to the body, then the above 1 through 3 apply, but step 4 does not seem to ever happen, so I need to detect this case and manually trigger the callbacks.

At least, this is what I'm observing in Chrome. I'm trying to find out exactly what's happening. Seems like the mutation events in the case of parsing due to document.body.insertAdjacentHTML('beforeend', '...') have fired before the parent element was upgraded, or something. (I'd need to find a way to attach the MutationObserver to the element before it is upgraded, but not sure how to do that because until now all my logic is in side the custom elements)

So now I have two cases to detect: parsing the initial payload, and parsing from insertAdjacentHTML().

The reason I want to detect these things is because in implementing childConnectedCallback and childDisconnectedCallback for my custom elements, I need to ensure that childConnectedCallback is only called once, in the same way that connectedCallback works (f.e. all callbacks called once a tree is inserted into a document, but not while the tree is outside of a document).

@rniwa
Copy link
Collaborator

rniwa commented Mar 18, 2019

yes, insertAdjacentHTML does use fragment parsing. In the case of fragment parsing, we use the upgrading path.

@trusktr
Copy link
Contributor Author

trusktr commented Mar 18, 2019

Ah okay, thanks. I wasn't familiar with the terminology (and google searches weren't very helpful). Can you point me to details regarding "the upgrading path"?

@rniwa
Copy link
Collaborator

rniwa commented Mar 18, 2019

I mean all the elements get created as a subtree then upgraded later. Just as if you made a subtree then append it via appendChild.

@trusktr
Copy link
Contributor Author

trusktr commented Mar 19, 2019

Just as if you made a subtree then append it via appendChild.

It's not quite the same as manual use of appendChild, because in that case I can make my elements with new and connect them together while they are custom elements. I can detect this case (using new ) just fine. EDIT: Ah, it's similar to using createElement, appendChild to make the tree and then insert it into the doc, and finally defining the elements after the fact.

And it also isn't the same initial-payload parsing (obviously). Here's the issue, I made two (EDIT: three) fiddles showing the difference between initial-payload parser, and fragment parser (and createElement/appendChild):

  1. this first one shows your technique from [feature request] change when upgrade/connected happens during parsing, so that it is the first event in the following microtask #787 (comment) works in initial parsing: https://jsfiddle.net/trusktr/psw530mf/1/
  2. and here's the same fiddle modified to use insertAdjacentHTML, and you'll see based on the output that MO does not fire initially: https://jsfiddle.net/trusktr/psw530mf/2/
  3. EDIT: I added a third case, manually using createElement and appendChild before defining elements, like my above EDIT mentioned. This one behaves like the insertAdjacentHTML case: https://jsfiddle.net/trusktr/psw530mf/3/

Seems like I need to detect the difference between the two parsings (EDIT: or maybe detect case 2 and 3 the same way), and in the second (and third) case I need to manually trigger child-handling logic.

@rniwa
Copy link
Collaborator

rniwa commented Mar 19, 2019

Just as if you made a subtree then append it via appendChild.

It's not quite the same as manual use of appendChild, because in that case I can make my elements with new and connect them together while they are custom elements. I can detect this case (using new ) just fine. EDIT: Ah, it's similar to using createElement, appendChild to make the tree and then insert it into the doc, and finally defining the elements after the fact.

I'm just saying that the behavior of upgrading is identical to appendChild, not saying that what you do or what you need to do in response to innerHTML is the same. Invoking document.createElement would synchronously construct a custom element so it's not same at all.

Anyway, I'm going to close this issue. This issue isn't really appropriate for standards discussion. There are no use cases presented, and there doesn't seem to be any API or spec change proposal.

@rniwa rniwa closed this as completed Mar 19, 2019
@trusktr
Copy link
Contributor Author

trusktr commented Mar 19, 2019

There are no use cases presented

Hey Ryo, the use case is presented, in the above 2nd fiddle. It's not clear how to detect or work with children in that case, in order to run logic on any/all upgraded children in a guaranteed way like we can with initial-payload parsing in the the 1st fiddle.

So as far as specs are concerned, can we modify fragment parser so it behaves like initial-payload parsing? That'd make things consistent, and that issue wouldn't exist.

@rniwa
Copy link
Collaborator

rniwa commented Mar 19, 2019

Hey Ryo, the use case is presented, in the above 2nd fiddle.

Wanting to detect when a custom element is created by a fragment parsing algorithm is NOT a use case. Wanting to have a guarantee that each child has been upgraded before accessing it is NOT a use case. A use case is a specific user scenario.

It's not clear how to detect or work with children in that case, in order to run logic on any/all upgraded children in a guaranteed way like we can with initial-payload parsing in the the 1st fiddle.

Like I've stated dozens of times, with the current custom elements API, you'd need to have a child talk to its parent, not a parent finding the right kind of children.

@rniwa
Copy link
Collaborator

rniwa commented Mar 19, 2019

Anyway, if you wanted to do something with every child custom element in a parent, you'd do something like this. I didn't debug too much but seems to mostly work.

<!DOCTYPE html>
<html>
<body>
<custom-parent><div></div><custom-child id="c1"></custom-child><span></span></custom-parent>
<script>

class CustomParentElement extends HTMLElement {

    isChildElementToWatch(node) { return node.localName == 'custom-child'; }

    constructor() {
        super();

        const nodesToCheck = [];
        for (let node = this.firstChild; node; node = node.nextSibling) {
            if (!this.isChildElementToWatch(node))
                continue;
            if (node.matches(':defined'))
                this.didConnectCustomChild(node);
            else
                nodesToCheck.push(node);
        }
        Promise.resolve().then(() => {
            for (const node of nodesToCheck) {
                if (node.matches(':defined'))
                    this.didConnectCustomChild(node);
            }
        });
        const observer = new MutationObserver((recordList) => {
            const addedNodes = new Set;
            for (const record of recordList) {
                for (const node of record.addedNodes) {
                    if (this.isChildElementToWatch(node))
                        addedNodes.add(node);
                }
                // This skips all the elements that got temporaily inserted and then removed.
                // Delete this code if you wanted to observe all children ever got inserted.
                for (const node of record.removedNodes) {
                    if (this.isChildElementToWatch(node))
                        addedNodes.delete(node);
                }
            }
            for (const node of addedNodes)
                this.didConnectCustomChild(node);
        });
        observer.observe(this, {childList: true});
    }

    didConnectCustomChild(child) {
        console.log(child);
    }

}
customElements.define('custom-parent', CustomParentElement);

class CustomChildElement extends HTMLElement { }
customElements.define('custom-child', CustomChildElement);

</script>
<custom-parent><div></div><custom-child id="c2"></custom-child><span></span></custom-parent>
<script>

const parent = document.createElement('custom-parent');
parent.appendChild(document.createElement('custom-child')).id = 'c3';
parent.innerHTML = '<custom-child id="c4"></custom-child>';

</script>
</body>
</html>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants