Last active
September 17, 2023 20:42
-
-
Save dy/ac2c4f1f92b69ce1e593ff6dfec4e89c to your computer and use it in GitHub Desktop.
Hyper/persistent document fragments
This file contains 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 characters
// extension of document fragment, keeps references to children | |
// based on https://github.com/WebReflection/document-persistent-fragment/blob/master/esm/document-persistent-fragment.js | |
class Bug extends DocumentFragment {} | |
const shenanigans = !(new Bug instanceof Bug); | |
export default class HyperFragment extends DocumentFragment { | |
#nodes = [] | |
// DocumentFragment overrides | |
constructor() { | |
super(); | |
if (shenanigans) return Object.setPrototypeOf(this, HyperFragment.prototype); | |
} | |
get children() { | |
return this.#nodes.filter(node => node instanceof Element); | |
} | |
get firstElementChild() { | |
return this.#nodes.find(node => node instanceof Element); | |
} | |
get lastElementChild() { | |
return this.#nodes.findLast(node => node instanceof Element); | |
} | |
get childElementCount() { | |
return this.children.length; | |
} | |
prepend(...nodes) { | |
nodes.forEach(node => { | |
removeItem(this.#nodes, node) | |
let before = this.#nodes[0] | |
this.#nodes.unshift(node) | |
this.isConnected ? this.parentNode.insertBefore(node, before) : super.insertBefore(node, before) | |
}); | |
} | |
append(...nodes) { | |
nodes.forEach(node => { | |
appendItem(this.#nodes, node) | |
this.isConnected ? this.parentNode.insertBefore(node, this.nextSibling) : super.appendChild(node) | |
}); | |
} | |
getElementById(id) { | |
return this.querySelector(`#${id}`); | |
} | |
querySelector(css) { | |
return this.isConnected ? this.parentNode.querySelector(css) : super.querySelector(css); | |
} | |
querySelectorAll(css) { | |
return this.isConnected ? this.parentNode.querySelectorAll(css) : super.querySelectorAll(css); | |
} | |
// Node overrides | |
get nodeName() { | |
return "#hyper-fragment"; | |
} | |
get isConnected() { | |
return this.#nodes.some(node => node.isConnected); | |
} | |
get parentNode() { | |
return this.#nodes.find(node => node.isConnected)?.parentNode; | |
} | |
get parentElement() { | |
return this.parentNode; | |
} | |
get childNodes() { | |
return Object.freeze(this.#nodes.slice(0)); | |
} | |
get firstChild() { | |
return this.#nodes[0]; | |
} | |
get lastChild() { | |
return this.#nodes[this.#nodes.length - 1]; | |
} | |
get previousSibling() { | |
return this.firstChild?.previousSibling; | |
} | |
get nextSibling() { | |
return this.lastChild?.nextSibling; | |
} | |
get textContent() { | |
return this.#nodes.map(node => node.textContent || '').join(''); | |
} | |
hasChildNodes() { | |
return 0 < this.#nodes.length; | |
} | |
cloneNode(deep) { | |
const frag = new HyperFragment; | |
frag.append(...this.#nodes.map(node => node.cloneNode(deep))); | |
return frag; | |
} | |
compareDocumentPosition(node) { | |
const {firstChild} = this; | |
return firstChild ? firstChild.compareDocumentPosition(node) : super.compareDocumentPosition(node); | |
} | |
contains(node) { | |
return this.#nodes.includes(node); | |
} | |
insertBefore(newNode, refNode) { | |
const nodes = this.#nodes; | |
const i = nodes.indexOf(refNode); | |
i > -1 ? nodes.splice(i, 0, newNode) : nodes.push(newNode); | |
return this.isConnected ? this.parentNode.insertBefore(newNode, refNode) : super.insertBefore(newNode, refNode); | |
} | |
appendChild(node) { | |
this.isConnected ? this.parentNode.insertBefore(node, this.nextSibling) : super.appendChild(node); | |
appendItem(this.#nodes, node); | |
return node; | |
} | |
replaceChild(replace, node) { | |
const nodes = this.#nodes, i = nodes.indexOf(node); | |
if (-1 < i) nodes[i] = replace; | |
return this.isConnected ? this.parentNode.replaceChild(replace, node) : super.replaceChild(replace, node); | |
} | |
removeChild(node) { | |
removeItem(this.#nodes, node); | |
return this.isConnected ? this.parentNode.removeChild(node) : super.removeChild(node); | |
} | |
remove() { | |
super.append(...this.#nodes); | |
} | |
valueOf() { | |
this.remove(); | |
return this; | |
} | |
} | |
function removeItem(nodes, node) { | |
const i = nodes.indexOf(node); | |
if (-1 < i) nodes.splice(i, 1); | |
} | |
function appendItem(nodes, node) { | |
removeItem(nodes, node); | |
nodes.push(node); | |
} |
This file contains 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 characters
import HyperFragment from '../src/fragment.js' | |
const dpf = new HyperFragment; | |
// for live debugging purpose | |
const id = 'test-id'; | |
const nodes = [ | |
document.createElement('p'), | |
document.createTextNode(''), | |
document.createElement('p'), | |
document.createElement('div'), | |
document.createElement('p') | |
]; | |
nodes.forEach((p, i) => p.textContent = (i + 1)); | |
nodes[2].id = id; | |
const previous = document.createTextNode('P'); | |
const next = document.createTextNode('N'); | |
document.body.appendChild(previous); | |
// console.assert(dpf.nodeName === '#document-persistent-fragment', 'correct name'); | |
// console.assert(dpf.nodeType === Node.DOCUMENT_PERSISTENT_FRAGMENT_NODE, 'correct type'); | |
console.assert(dpf.firstElementChild == null, 'no firstElementChild by default'); | |
console.assert(dpf.lastElementChild == null, 'no lastElementChild by default'); | |
console.assert(dpf.childElementCount === 0, 'childElementCount is 0 by default'); | |
console.assert(dpf.children.length === 0, 'no children by default'); | |
console.assert(dpf.childNodes.length === 0, 'no childNodes by default'); | |
console.assert(dpf.firstChild == null, 'no firstChild by default'); | |
console.assert(dpf.lastChild == null, 'no lastChild by default'); | |
console.assert(dpf.hasChildNodes() === false, 'no childNodes by default'); | |
console.assert(30 < dpf.compareDocumentPosition(previous), 'correct compareDocumentPosition'); | |
console.assert(dpf.contains(nodes[0]) === false, 'no nodes contained'); | |
dpf.append(nodes[0]); | |
console.assert(dpf.contains(nodes[0]) === true, 'some nodes contained'); | |
console.assert(dpf.hasChildNodes() === true, 'it has childNodes'); | |
console.assert(dpf.firstElementChild === nodes[0], 'nodes[0] as firstElementChild'); | |
console.assert(dpf.lastElementChild === nodes[0], 'nodes[0] as lastElementChild'); | |
console.assert(dpf.childElementCount === 1, '1 childElementCount'); | |
console.assert(dpf.children.length === 1, '1 children.length'); | |
console.assert(dpf.children.length === 1, '1 childNodes.length'); | |
console.assert(dpf.firstChild === dpf.lastChild, 'firstChild same as lastChild with 1 node'); | |
dpf.prepend(nodes[1]); | |
console.assert(dpf.firstElementChild === nodes[0], 'nodes[0] still as firstElementChild'); | |
console.assert(dpf.lastElementChild === nodes[0], 'nodes[0] still as lastElementChild'); | |
console.assert(dpf.childElementCount === 1, '1 still as childElementCount'); | |
console.assert(dpf.childNodes.length === 2, '2 childNodes.length'); | |
console.assert(dpf.childNodes[0] === nodes[1], 'nodes[1] as childNodes[0]'); | |
console.assert(dpf.childNodes[0] === dpf.firstChild, 'nodes[1] as firstChild'); | |
console.assert(dpf.children[0] === dpf.lastChild, 'nodes[0] as lastChild'); | |
dpf.append(nodes[1]); | |
console.assert(dpf.childNodes[0] === nodes[0], 'nodes[0] as childNodes[0]'); | |
console.assert(dpf.childNodes[1] === nodes[1], 'nodes[1] as childNodes[1]'); | |
console.assert(dpf.childNodes.length === 2, '2 still as childNodes.length'); | |
dpf.append(...nodes); | |
console.assert(dpf.childElementCount === 4, '4 childElementCount'); | |
console.assert(dpf.childNodes.length === 5, '5 childNodes.length'); | |
console.assert(dpf.getElementById(id) === nodes[2], 'getElementById is OK'); | |
console.assert(dpf.getElementById('nope') === null, 'getElementById returns null if not found'); | |
console.assert(dpf.querySelector(`#${id}`) === nodes[2], 'querySelector is OK'); | |
console.assert(dpf.querySelectorAll(`p`).length === 3, 'querySelectorAll returns 3 <p>'); | |
console.assert(dpf.isConnected === false, 'not connected'); | |
console.assert(dpf.previousSibling === null, 'no previousSibling'); | |
console.assert(dpf.nextSibling === null, 'no nextSibling'); | |
// LIVE | |
console.assert(document.body.appendChild(dpf) === dpf, 'Element can append a DPF'); | |
console.assert(dpf.isConnected === true, 'connected'); | |
console.assert(dpf.childElementCount === 4, '4 childElementCount'); | |
console.assert(dpf.childNodes.length === 5, '5 childNodes.length'); | |
console.assert(dpf.getElementById(id) === nodes[2], 'getElementById is OK'); | |
console.assert(dpf.getElementById('nope') === null, 'getElementById returns null if not found'); | |
console.assert(dpf.querySelector(`#${id}`) === nodes[2], 'querySelector is OK'); | |
console.assert(dpf.querySelectorAll(`p`).length === 3, 'querySelectorAll returns 3 <p>'); | |
console.assert(dpf.parentElement === document.body, 'correct parentElement/Node'); | |
console.assert(dpf.previousSibling === previous, 'correct previousSibling'); | |
document.body.appendChild(next); | |
console.assert(dpf.compareDocumentPosition(previous) === 2, 'correct previous compareDocumentPosition'); | |
console.assert(dpf.compareDocumentPosition(next) === 4, 'correct next compareDocumentPosition'); | |
console.assert(dpf.nextSibling === next, 'correct nextSibling'); | |
console.assert(dpf.textContent === '12345', 'correct textContent'); | |
const clone = dpf.cloneNode(true); | |
console.assert(clone.isConnected === false, 'clone not connected'); | |
console.assert(dpf.textContent === clone.textContent, 'clone textContent'); | |
document.body.insertBefore(clone, dpf.nextSibling); | |
console.assert(clone.isConnected === true, 'clone is connected'); | |
console.assert(document.body.textContent.trim() === 'P1234512345N', 'correct doubled body text'); | |
clone.remove(); | |
console.assert(document.body.textContent.trim() === 'P12345N', 'correct body text'); | |
console.assert(clone.isConnected === false, 'clone is disconnected'); | |
dpf.removeChild(nodes[2]); | |
dpf.removeChild(nodes[3]); | |
console.assert(document.body.textContent.trim() === 'P125N', 'smaller body text'); | |
dpf.appendChild(nodes[2]); | |
console.assert(document.body.textContent.trim() === 'P1253N', 'bigger body text'); | |
dpf.replaceChild(nodes[3], nodes[1]); | |
console.assert(document.body.textContent.trim() === 'P1453N', 'different body text'); | |
const asDPF = node => (isDPF(node) ? node.valueOf() : node); | |
// This is fully based on window/global patching side effect. | |
// Do not import DocumentPersistentFragment upfront or shenanigans happen. | |
const isDPF = node => node instanceof HyperFragment; | |
const { appendChild, removeChild, insertBefore, replaceChild } = Node.prototype; | |
Object.assign( | |
Node.prototype, | |
{ | |
appendChild(node) { | |
return appendChild.call(this, asDPF(node)); | |
}, | |
removeChild(node) { | |
if (isDPF(node) && node.parentNode === this) { | |
node.remove(); | |
} else { | |
removeChild.call(this, node); | |
} | |
return node; | |
}, | |
insertBefore(before, node) { | |
return insertBefore.call(this, asDPF(before), node); | |
}, | |
replaceChild(replace, node) { | |
return replaceChild.call(this, asDPF(replace), node); | |
} | |
} | |
); | |
document.body.removeChild(dpf); | |
console.assert(document.body.textContent.trim() === 'PN', 'tiny body text'); | |
dpf.append(...nodes); | |
console.assert(dpf.textContent === '12345', 'correct dpf text'); | |
dpf.removeChild(nodes[2]); | |
dpf.removeChild(nodes[3]); | |
console.assert(dpf.textContent === '125', 'smaller dpf text'); | |
dpf.appendChild(nodes[2]); | |
console.assert(dpf.textContent === '1253', 'bigger dpf text'); | |
dpf.replaceChild(nodes[3], nodes[1]); | |
console.assert(dpf.textContent === '1453', 'different dpf text'); | |
document.body.lastChild.replaceWith(dpf); | |
console.assert(document.body.textContent.trim() === 'P1453', 'final body text'); | |
dpf.append(...nodes); | |
document.body.textContent = 'OK'; |
Hi @michTheBrandofficial! It seems dealing with not working code is one of the challenges for framework authors.
I never finished this hyperfragment, that was a rabbit hole so I gave up and put into gist.
You can take it from current state and try to fix the bugs forward on if you want.
Exactly 💯. But there's no problem. I have read, refactored and rewrote the code in Typescript for type safety.
I was really worried that I was the only person thinking about this persistent fragment of the DOM
Anyway, if you'd like to see how I did it, you can read the code at Dom love fragment
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Hello, I am Charles Ikechukwu.
The creator of the NixixJS framework, I have tried this code snippet and it doesnt seem to work. Please can you explain it a bit?