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

How to wait for will-be custom elements to be upgraded? #558

Closed
trusktr opened this issue Aug 29, 2016 · 5 comments
Closed

How to wait for will-be custom elements to be upgraded? #558

trusktr opened this issue Aug 29, 2016 · 5 comments

Comments

@trusktr
Copy link
Contributor

trusktr commented Aug 29, 2016

In my experiences so far, I define custom elements, and the <body> of the page is empty. At some point in the future after the custom elements are defined and after the document is loaded, I use a view layer (f.e. React, Meteor Blaze, etc) to render some components which results in elements being added into the <body>. This works really well because by the time my custom elements get placed into the DOM (indirectly, by the view layers, not by me), then the registered Custom Element classes have their life cycle callbacks (I'm on v0, so createdCallback, attachedCallback, detachedCallback, and attributeChangedCallback) fired as expected and in the correct order.

It is a completely different story when we're not using a view layer. Suppose we're sending HTML markup from the server. The HTML markup is parsed at some point in time which can possibly before the Custom Element classes are ever registered. What can happen is that when the Custom Element classes are finally registered, the DOM tree may already be created, which causes attachedCallbacks to fire in possibly the incorrect order because if there are multiple custom element classes that should be registered, then what happens is that the attachedCallback of one element may be called but not that of the other elements that are not yet upgraded, which causes problems with a framework that defines multiple elements.

Another thing is that if Custom Element classes are registered after the elements already exist in DOM, then their attributeChangedCallback methods will not be fired with the initially set attribute values (if any). Note, I am testing this with Chrome's native v0 implementation, and that is the behavior that I observe.

Let me explain how the problems can be solved by showing you some hacks I've written:

The following is an excerpt from a class, with some code removed for simplicity just to show the problem. What you'll see is that in the attachedCallback I've implemented some code that will manually call attributeChangedCallback in the case that there might be pre-existing attributes on the element once it has been upgraded (Chrome's implementation does not call attributeChangedCallback in this case). You'll also notice that I've written a polyfill for my childConnectedCallback idea, and in the implementation you'll see that I'm using setTimeout, which is an ugly and horrible hack in order to defer some code so that the code can be executed after upgrade of this element's children has been completed, otherwise the callbacks may be executed before the child elements are upgraded (i.e. before the child element classes are registered):

        attachedCallback() {

            // Handle any nodes that may have been connected before `this` node
            // was created (f.e. child nodes that were connected before the
            // custom elements were registered and which would therefore not be
            // detected by the following MutationObserver).
            if (this.childNodes.length) {
                console.log(` ------ ${this.nodeName} has children!!!`)

                // Timeout needed in case the Custom Elements classes are
                // registered after the elements are already defined in the DOM
                // but not yet upgraded.
                setTimeout(() => {
                    for (let node of this.childNodes) {
                        this.childConnectedCallback(node)
                    }
                }, 5)
            }

            // TODO issue #40
            // Observe nodes in the future.
            // This one doesn't need atimeout since the observation is already
            // async.
            const observer = new MutationObserver(changes => {
                for (let change of changes) {
                    if (change.type != 'childList') continue

                    for (let node of change.addedNodes)
                        this.childConnectedCallback(node)

                    for (let node of change.removedNodes)
                        this.childDisconnectedCallback(node)
                }
            })
            observer.observe(this, { childList: true })

            // fire this.attributeChangedCallback in case some attributes have
            // existed before the custom element was upgraded.
            if (this.hasAttributes())
                for (let attr of this.attributes)
                    if (this.attributeChangedCallback)
                        this.attributeChangedCallback(attr.name, null, attr.value)
        }

Subclasses of the class shown above implement childConnectedCallback, and rely on their children being instances of my framework's custom element classes.

Again, this all works fine if my classes are registered before any of these elements are ever placed into the DOM. But, if the elements exist prior to registration, they will need to be upgraded, and then my code would not work without the setTimeout hack and the manual calling of the attributeChangedCallback methods.

This has led me to ask the following question on stackoverflow: How to wait for Custom Element reference to be “upgraded”?.

I believe if there were some official way to wait for the upgrade of an element if all you have is a reference to that element (without the ability to modify that element's code prior), then it might make it easier to write code in a way that isn't so hacky as in my above example.

It may be possible that an element is never registered, so maybe there would also need to be a way to cancel the waiting for upgrade, in order not to leak memory.

Assume that the custom elements that I will await to be upgraded are third party elements. I don't want to monkey patch the createdCallback of the element whose reference I have, as that would also be hacky.

Is there any way to do something after an element has been upgraded?

Is there any other recommended approach?

Previously, I simply placed logic into my child elements that on attachedCallback would look for their parents in order to create a connection with their parents, but this fails badly with closed shadow trees as described in #527, so I've since changed my API so that it is parents that observe children in order to create connections with the children.

However!

The parent cannot create a connection with the children when the parent class is registered and the child class is not yet registered because the parent depends on custom properties existing on the upgraded children, which is why I've written the above hacks. This scenario seems to happen due to the fact that element registration seems to be synchronous.

Suppose we have this code:

document.registerElement('some-el', SomeElement)
document.registerElement('other-el', OtherElement)

The first line will cause some-el elements to be upgraded synchrously, and those elements will therefore execute logic immediately, and if that logic depends on child elements being other-el, then the logic will fail. However, if I defer the logic, then that gives a chance for the registration of other-el to happen first, and then the logic in the parent will succeed.

Once again, this is not a problem at all when the elements are registered before any of the elements are ever placed into the DOM. For example, when I use React, then React will "render" my custom elements into the DOM in the future after my custom element classes have already been registered. However, I've also made a "global" version of my library, and in one case I am simply sending the markup from the server (not using a client-side view layer), so what happens is that Chrome's v0 document.registerElement API seems to upgrade elements after they already exist in the DOM (it will need to upgrade the elements rather than instantiate them from my custom classes).

@trusktr
Copy link
Contributor Author

trusktr commented Aug 29, 2016

I documented a possibly solution in #559, but note I may be incorrect about my assumptions of how the upgrade process works, and I may be experiencing only the sideeffects of the Chrome v0 implementation.

@rniwa
Copy link
Collaborator

rniwa commented Aug 29, 2016

Another thing is that if Custom Element classes are registered after the elements already exist in DOM, then their attributeChangedCallback methods will not be fired with the initially set attribute values (if any).

This is explicitly not the case in v1 API. We would always invoke attributeChangedCallback on every attribute including ones that were present at the time of upgrade. If that's not the case, then we need to file a spec bug and fix that.

@kojiishi
Copy link

Do whenDefined and :defined help? See an example in the spec.

Promise.all(
      [...articleContainer.querySelectorAll(":not(:defined)")]
        .map(el => customElements.whenDefined(el.localName))

@domenic
Copy link
Collaborator

domenic commented Aug 16, 2017

whenDefined() is the answer here, indeed.

@domenic domenic closed this as completed Aug 16, 2017
@trusktr
Copy link
Contributor Author

trusktr commented Jan 28, 2018

@domenic @kojiishi whenDefined() only works when the library author knows the names that library constructors will be assigned to.

This is currently difficult to work with without using ugly deferral hacks and leaking memory.

@rniwa's whenUpgraded(elementInstance) idea would help here, but it can still leak memory if the element names associated with constructors are not known.

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

No branches or pull requests

4 participants