|
/** |
|
* 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) |
|
} |
|
} |
|
} |
The whole point of this snippet is to keep nodes in their pre-Vue state, so accessing them before Vue is initialized is probably the best option.
Having elements available in the
mounted
hook is not possible, because at that point in time Vue has freshly rendered its template, meaning at that point the original nodes are most definitly gone. TheMutationObserver
only kicks in sometime after that which will lead to the nodes being restored.However, since the MutationObserver is inherently and deliberately asynchronous, it's impossible to reliably predict when it's going to trigger.
What you could theoretically do is emitting an event on the hosting Vue component whenever the script re-inserts the removed nodes:
Usage: