Last active
October 2, 2023 13:48
-
-
Save loilo/a3ec6dbb78d42594b45ec7ebd00139c7 to your computer and use it in GitHub Desktop.
Revisions
-
loilo revised this gist
Jan 9, 2019 . 1 changed file with 2 additions and 2 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal 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, "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>Slot content I don't have control over</p> </dom-keep-alive> </interactive-element> ``` -
loilo revised this gist
Jan 9, 2019 . 1 changed file with 2 additions and 2 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal 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>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 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. -
loilo revised this gist
Jan 9, 2019 . 1 changed file with 1 addition and 1 deletion.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal 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, // ... }) -
loilo revised this gist
Apr 26, 2018 . 1 changed file with 1 addition and 1 deletion.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal 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 = dka.prepareRenderTarget(el) // Activate Vue const vm = new Vue({ -
loilo created this gist
Apr 26, 2018 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal 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">`. This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal 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) } } }