Re: [w3c/webcomponents] [feature request] change when upgrade/connected happens during parsing, so that it is the first event in the following microtask (#787)

Alright, the following is the final solution with `MutationObserver` which seems to work in all the cases where the custom elements are defined BEFORE they are used, cleans itself up, and triggers `childConnectedCallback` and `childDisconnectedCallback` following the same pattern as `connectedCallback` and `disconnectedCallback` wherein they are only called when element is connected/disconnected into/from a context (document or shadow root).

Don't mind the `Class()` syntax which is using class inheritance from [lowclass](https://github.com/trusktr/lowclass). The `Mixin()` helper simply makes a mixin so that the class has a static `mixin` method). Other than this, you can imagine it using plain `class` syntax.

The implementation of `observeChildren` is [here](https://github.com/trusktr/infamous/blob/b985ec7bf8a1f1f8ace2d4051d7d522f9a1ce988/src/core/Utility.js#L15-L83), which is just using `MutationObserver` with `childList: true` to call the connected and disconnected callbacks that are passed into `observeChildren`.

```js
import Class from 'lowclass'
import Mixin from 'lowclass/Mixin'
import { observeChildren } from '../core/Utility'

const childConnectedEvent = new CustomEvent('child-connected', { bubbles: false, composed: false })
const childDisconnectedEvent = new CustomEvent('child-disconnected', { bubbles: false, composed: false })

export default
Mixin(Base => Class('WithChildren').extends(Base, ({ Super, Private, Public }) => ({

    // TODO, if the polyfills cover this remove it, isConnected is already part
    // of the window.Node class (in the specs) and serves the same purpose.
    // https://github.com/webcomponents/webcomponentsjs/issues/1065
    isConnected: false,

    constructor(...args) {
        const self = Super(this).constructor(...args)
        Private(self).__createObserver()
        return self
    },

    connectedCallback() {
        this.isConnected = true
        Super(this).connectedCallback && Super(this).connectedCallback()

        const priv = Private(this)

        // NOTE! This specifically handles the case that if the node was previously
        // disconnected from the document, then it won't have an __observer when
        // reconnected. This is not the case when the element is first created,
        // in which case the constructor already created the __observer.
        //
        // So in this case we have to manually trigger childConnectedCallbacks
        if (!priv.__observer) {
            const currentChildren = this.children
            Promise.resolve().then(() => {
                for (let l=currentChildren.length, i=0; i<l; i+=1) {
                    this.childConnectedCallback && this.childConnectedCallback(currentChildren[i])
                }
            })
        }

        Private(this).__createObserver()
    },

    disconnectedCallback() {
        this.isConnected = false
        Super(this).disconnectedCallback && Super(this).disconnectedCallback()

        // Here we have to manually trigger childDisconnectedCallbacks
        // because the observer will be disconnected.
        const lastKnownChildren = this.children
        Promise.resolve().then(() => {
            for (let l=lastKnownChildren.length, i=0; i<l; i+=1) {
                this.childDisconnectedCallback && this.childDisconnectedCallback(lastKnownChildren[i])
            }
        })

        Private(this).__destroyObserver()
    },

    // private fields
    private: {
        __observer: null,
        __handledChildren: [],

        __createObserver() {
            if (this.__observer) return

            const self = Public(this)

            this.__observer = observeChildren(
                self,
                child => {
                    if (!self.isConnected) return

                    if (!this.__handledChildren.includes(child))
                        this.__handledChildren.push(child)

                    self.childConnectedCallback && self.childConnectedCallback(child)
                },
                child => {
                    if (!self.isConnected) return

                    const indexOfChild = this.__handledChildren.indexOf(child)
                    if (indexOfChild < 0)
                        this.__handledChildren.splice(indexOfChild, 1)

                    self.childDisconnectedCallback && self.childDisconnectedCallback(child)
                },
                true
            )
        },

        __destroyObserver() {
            if (!this.__observer) return
            this.__observer.disconnect()
            this.__observer = null
        },
    },
})))
```

And here's how to use it:

```js
import WithChildren from './WithChildren'

class MyElement extends WithChildren.mixin(HTMLElement) {
  childConnectedCallback(child) { ... }
  childDisconnectedCallback(child) { ... }
}
```

-- 
You are receiving this because you are subscribed to this thread.
Reply to this email directly or view it on GitHub:
https://github.com/w3c/webcomponents/issues/787#issuecomment-459829842

Received on Friday, 1 February 2019 18:57:49 UTC