Skip to content

Instantly share code, notes, and snippets.

@loilo
Last active October 2, 2023 13:48
Show Gist options
  • Select an option

  • Save loilo/a3ec6dbb78d42594b45ec7ebd00139c7 to your computer and use it in GitHub Desktop.

Select an option

Save loilo/a3ec6dbb78d42594b45ec7ebd00139c7 to your computer and use it in GitHub Desktop.

Revisions

  1. loilo revised this gist Jan 9, 2019. 1 changed file with 2 additions and 2 deletions.
    4 changes: 2 additions & 2 deletions DomKeepAlive.md
    Original file line number Diff line number Diff line change
    @@ -8,7 +8,7 @@ For my use cases, Vue has one critical pitfall: I frequently have/want to use Vu
    > ```
    > *I need to activate the Vue component `<interactive-element>`.*
    In particular, "content I don't have control over" may include interactive elements with attached event listeners etc. This content will be re-rendered when activating Vue and subsequently, all previous modifications to its DOM — including any DOM references and event listeners — will be lost:
    In particular, "slot content I don't have control over" may include interactive elements with attached event listeners etc. This content will be re-rendered when activating Vue and subsequently, all previous modifications to its DOM — including any DOM references and event listeners — will be lost:
    ```javascript
    document.querySelector('p').onclick = function () {
    @@ -29,7 +29,7 @@ That's where this script comes in. It exposes a `DomKeepAlive` class and an acco
    ```html
    <interactive-element>
    <dom-keep-alive>
    <p>Content I don't have control over</p>
    <p>Slot content I don't have control over</p>
    </dom-keep-alive>
    </interactive-element>
    ```
  2. loilo revised this gist Jan 9, 2019. 1 changed file with 2 additions and 2 deletions.
    4 changes: 2 additions & 2 deletions DomKeepAlive.md
    Original file line number Diff line number Diff line change
    @@ -3,7 +3,7 @@ For my use cases, Vue has one critical pitfall: I frequently have/want to use Vu

    > ```html
    > <interactive-element>
    > <p>Content I don't have control over</p>
    > <p>Slot content I don't have control over</p>
    > </interactive-element>
    > ```
    > *I need to activate the Vue component `<interactive-element>`.*
    @@ -57,7 +57,7 @@ const vm = new Vue({
    init(vm.$el)
    ```

    This will prevent the content inside the `<dom-keep-alive>` from every being re-rendered by Vue.
    This will prevent the content inside the `<dom-keep-alive>` from ever being re-rendered by Vue.

    Some more things to note:
    * The script uses MutationObservers to detect entrance or removal of the contained nodes, so it works even when a `<dom-keep-alive>` lives inside a `v-if` branch of the surrounding component.
  3. loilo revised this gist Jan 9, 2019. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion DomKeepAlive.md
    Original file line number Diff line number Diff line change
    @@ -49,7 +49,7 @@ const init = dka.prepareRenderTarget(el)

    // Activate Vue
    const vm = new Vue({
    el: target,
    el,
    // ...
    })

  4. loilo revised this gist Apr 26, 2018. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion DomKeepAlive.md
    Original file line number Diff line number Diff line change
    @@ -45,7 +45,7 @@ const dka = new DomKeepAlive(Vue)
    const el = document.querySelector('interactive-element')

    // Get an initializer for the <dom-keep-alive> nodes *before* activating Vue
    const init = DomKeepAlive.prepareRenderTarget(el)
    const init = dka.prepareRenderTarget(el)

    // Activate Vue
    const vm = new Vue({
  5. loilo created this gist Apr 26, 2018.
    66 changes: 66 additions & 0 deletions DomKeepAlive.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,66 @@
    # Keep pre-existing DOM alive in Vue
    For my use cases, Vue has one critical pitfall: I frequently have/want to use Vue components with `<slot>`s as wrappers for content from a CMS which I don't have control over. That is, the content comes over the wire via HTML, and I have to activate Vue for some of it.

    > ```html
    > <interactive-element>
    > <p>Content I don't have control over</p>
    > </interactive-element>
    > ```
    > *I need to activate the Vue component `<interactive-element>`.*
    In particular, "content I don't have control over" may include interactive elements with attached event listeners etc. This content will be re-rendered when activating Vue and subsequently, all previous modifications to its DOM — including any DOM references and event listeners — will be lost:
    ```javascript
    document.querySelector('p').onclick = function () {
    alert('Hello world!')
    }
    // Clicking the <p> will greet the world
    new Vue({ el: 'interactive-element' })
    // Clicking the <p> will no longer do anything
    ```
    That's where this script comes in. It exposes a `DomKeepAlive` class and an according `<dom-keep-alive>` Vue component. Unfortunately, just a Vue component won't do the trick, so the activation routine is a little more tricky:

    1. Add `<dom-keep-alive>` wrappers to the HTML we have control over (the wrapper):

    ```html
    <interactive-element>
    <dom-keep-alive>
    <p>Content I don't have control over</p>
    </dom-keep-alive>
    </interactive-element>
    ```

    2. Add some action before and after throwing Vue onto the `<interactive-element>`:
    ```javascript
    // Somewhere in your script
    const dka = new DomKeepAlive(Vue)

    // For each of the Vue instances to activate:

    // Pick the element that holds the future Vue instance
    const el = document.querySelector('interactive-element')

    // Get an initializer for the <dom-keep-alive> nodes *before* activating Vue
    const init = DomKeepAlive.prepareRenderTarget(el)

    // Activate Vue
    const vm = new Vue({
    el: target,
    // ...
    })

    // Run the initializer on the Vue root node
    init(vm.$el)
    ```

    This will prevent the content inside the `<dom-keep-alive>` from every being re-rendered by Vue.

    Some more things to note:
    * The script uses MutationObservers to detect entrance or removal of the contained nodes, so it works even when a `<dom-keep-alive>` lives inside a `v-if` branch of the surrounding component.
    * You may have multiple `<dom-keep-alive>` wrappers inside the same Vue component. However, don't nest them.
    * `<dom-keep-alive>` content is pretty much handled like `v-pre` — you cannot use interactive Vue syntax inside it.
    * An actual DOM element needs to be rendered in place of a `<dom-keep-alive>` placeholder. By default, this will be a `<div>`, but you can easily change that with the familiar Vue syntax: `<dom-keep-alive is="span">`.
    172 changes: 172 additions & 0 deletions DomKeepAlive.ts
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,172 @@
    /**
    * Handles DOM nodes to keep alive inside a Vue component
    */
    class DomKeepAlive {
    /**
    * Holds a mapping of all generated IDs to their according child node fragments
    */
    protected fragments = new Map<string, DocumentFragment>()

    /**
    * Registers the <dom-keep-alive /> component in Vue
    * @param Vue The Vue constructor to use
    */
    public constructor (Vue: any) {
    if (!('dom-keep-alive' in Vue.options.components)) {
    Vue.component('dom-keep-alive', {
    render: createElement => createElement('div')
    })
    }
    }

    /**
    * Generates a random ID
    * @return The generated ID
    */
    protected generateId (): string {
    // @ts-ignore
    return `dom-keep-alive--${([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, c => (c ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))).toString(16))}`
    }

    /**
    * Takes the child nodes of an element and transfers them to a DocumentFragment
    * @param target The element to take child nodes from
    * @return The DocumentFragment containing the target's child nodes
    */
    protected childNodesToFragment (target: Element) {
    const frag = document.createDocumentFragment()
    for (const node of Array.from(target.childNodes)) {
    frag.appendChild(node)
    }
    return frag
    }

    /**
    * Checks if the given element contains an element with the given ID (or has the ID itself)
    * @param target The target for looking up the ID
    * @param id The ID to check for
    * @return The element with the present ID, null otherwise
    */
    protected containsId (target: Element, id: string) {
    return target.id === id
    ? target
    : target.querySelector('#' + id)
    }

    /**
    * Waits (via MutationObserver) for a certain element ID to be present/gone from the subtree of a target. Resolves immediately if the ID is present/gone from the beginning.
    * @param target The element that will be observed for the given ID to be present/gone
    * @param id The element ID to look out for
    * @param present If to look for an added (true) or removed (false) ID
    * @return Resolves when the ID is present/gone
    */
    protected waitForId (target: Element, id: string, present: boolean) {
    return new Promise<Element|null>((resolve, reject) => {
    const el = this.containsId(target, id)

    if (present ? el : !el) {
    resolve(present ? el : null)
    } else {
    const observer = new MutationObserver(mutations => {
    let foundElement = null

    beforeMutations: {
    for (const mutation of mutations) {
    for (const node of mutation[`${present ? 'added' : 'removed'}Nodes`]) {
    if (node instanceof Element) {
    const el = this.containsId(node, id)

    if (el) {
    foundElement = el
    break beforeMutations
    }
    }
    }
    }
    }

    if (foundElement) {
    observer.disconnect()
    resolve(foundElement)
    }
    })

    observer.observe(target, { childList: true, subtree: true })
    }
    })
    }

    /**
    * Waits for a certain element ID to be present from the subtree of a target. Resolves immediately if the ID is present from the beginning.
    * @param target The element that will be observed for the given ID to be present
    * @param id The element ID to look out for
    * @return Resolves when the ID is present
    */
    protected waitForIdPresent (target: Element, id: string) {
    return this.waitForId(target, id, true) as Promise<Element>
    }

    /**
    * Waits for a certain element ID to be gone from the subtree of a target. Resolves immediately if the ID is not present from the beginning.
    * @param target The element that will be observed for the given ID to be gone
    * @param id The element ID to look out for
    * @return Resolves when the ID is gone
    */
    protected waitForIdGone (target: Element, id: string) {
    return this.waitForId(target, id, false)
    }

    /**
    * Prepare a render target element with all its contained keep-alive nodes
    * @param renderTarget The render root node
    * @return An initializer function that can be passed the Vue root node and handles contained keep-alive nodes
    */
    public prepareRenderTarget (renderTarget: Element) {
    const ids = []
    for (const element of Array.from(renderTarget.querySelectorAll('dom-keep-alive'))) {
    ids.push(this.prepareKeepAlive(element))
    }

    return (vueRoot: Element) => this.initKeepAliveLifecycles(vueRoot, ids)
    }

    /**
    * Prepare a single keep-alive element
    * @param keepAliveElement The keep-alive node to prepare
    * @return The ID generated for the keep-alive node
    */
    public prepareKeepAlive (keepAliveElement: Element) {
    const id = this.generateId()
    keepAliveElement.setAttribute('id', id)
    this.fragments.set(id, this.childNodesToFragment(keepAliveElement))
    return id
    }

    /**
    * Initialize the lifecycle of a keep-alive node inside a render target
    * @param renderTarget The render target to check for the keep-alive node
    * @param keepAliveId The ID the surveilled keep-alive node will have
    */
    public async initKeepAliveLifecycle (renderTarget, keepAliveId) {
    const appearedEl = await this.waitForIdPresent(renderTarget, keepAliveId)
    if (this.fragments.has(keepAliveId)) {
    const frag = this.fragments.get(keepAliveId)
    appearedEl.appendChild(frag)
    }

    const removedEl = await this.waitForIdGone(renderTarget, keepAliveId)
    this.fragments.set(keepAliveId, this.childNodesToFragment(removedEl))
    this.initKeepAliveLifecycle(renderTarget, keepAliveId)
    }

    /**
    * Initialize the lifecycle of multiple keep-alive nodes inside a render target
    * @param renderTarget The render target to check for keep-alive nodes
    * @param keepAliveIds The IDs the surveilled keep-alive nodes
    */
    public initKeepAliveLifecycles (target: Element, keepAliveIds: string) {
    for (const id of keepAliveIds) {
    this.initKeepAliveLifecycle(target, id)
    }
    }
    }