Skip to content

Instantly share code, notes, and snippets.

@dy
Last active September 17, 2023 20:42
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dy/ac2c4f1f92b69ce1e593ff6dfec4e89c to your computer and use it in GitHub Desktop.
Save dy/ac2c4f1f92b69ce1e593ff6dfec4e89c to your computer and use it in GitHub Desktop.
Hyper/persistent document fragments
// 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);
}
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';
@michTheBrandofficial
Copy link

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?

@dy
Copy link
Author

dy commented Sep 13, 2023

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.

@michTheBrandofficial
Copy link

Exactly 💯. But there's no problem. I have read, refactored and rewrote the code in Typescript for type safety.

@michTheBrandofficial
Copy link

I was really worried that I was the only person thinking about this persistent fragment of the DOM

@michTheBrandofficial
Copy link

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