|
/** |
|
* 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) |
|
} |
|
} |
|
} |
Ok thanks. The reason I want to use in the mounted hook is because I'm using Vue for some controls and managing component states, and want to use the state to change the kept alive nodes. In your code
renderTarget
is of typeElement
, i.e.$emit
doesnt' work. I don't think the current implementation has a reference to theDomKeepAlive
-instance nor the root component, right?