Re: [whatwg/dom] Proposal: a DocumentFragment whose nodes do not get removed once inserted (#736)

OK, I went ahead and created this implementation which leaves just a few details to discuss but it works already great:

**persistent-fragment.js**
```js
import native from 'https://esm.run/custom-function/factory';

const OWNERSHIP = 'ownerFragment';
const { defineProperty, getPrototypeOf, hasOwn } = Object;

const children = ({ nodeType }) => nodeType === 1;
const isOwned = (self, node) => !hasOwn(node, OWNERSHIP) || node[OWNERSHIP] == self;
const asValueOf = node => node instanceof PersistentFragment ? node.valueOf() : node;

export default class PersistentFragment extends native(DocumentFragment) {
  #childNodes;
  constructor(...childNodes) {
    super(document.createDocumentFragment());
    this.#childNodes = childNodes.map(asOwnedNode, this);
    super.append(...childNodes);
  }
  append(...childNodes) {
    childNodes = childNodes.map(asOwnedNode, this);
    if (this.#childNodes.length)
      this.#childNodes.at(-1).after(...childNodes);
    else
      super.append(...childNodes);
    this.#childNodes.push(...childNodes);
  }
  appendChild(childNode) {
    const node = asOwnedNode.call(this, childNode);
    if (this.#childNodes.length)
      this.#childNodes.at(-1).after(node);
    else
      super.appendChild(node);
    this.#childNodes.push(node);
  }
  insertBefore(childNode, ownedNode) {
    if (ownedNode) {
      if (isOwned(this, ownedNode)) {
        ownedNode.before(asOwnedNode.call(this, childNode));
        const i = this.#childNodes.indexOf(ownedNode);
        this.#childNodes.splice(i, 0, childNode);
      }
      else throw new Error('Illegal operation');
    }
    else this.appendChild(childNode);
  }
  prepend(...childNodes) {
    childNodes = childNodes.map(asOwnedNode, this);
    if (this.#childNodes.length)
      this.#childNodes.at(0).before(...childNodes);
    else
      super.prepend(...childNodes);
    this.#childNodes.unshift(...childNodes);
  }
  removeChild(childNode) {
    const i = this.#childNodes.indexOf(ownedNode);
    if (i < 0) throw new Error('Illegal operation');
    childNode.remove();
    this.#childNodes.splice(i, 1);
  }
  replaceChildren(...childNodes) {
    this.#childNodes.forEach(notOwnedAnymore, this);
    this.#childNodes = childNodes.map(asOwnedNode, this);
    super.replaceChildren(...this.#childNodes);
  }
  valueOf() {
    if (this.#childNodes.at(0)?.parentNode !== this)
      super.append(...this.#childNodes);
    return this;
  }
}

function asOwnedNode(childNode) {
  if (!isOwned(this, childNode))
    throw new Error('Illegal operation');
  return defineProperty(childNode, OWNERSHIP, {
    value: this,
    configurable: true
  });
}

function notOwnedAnymore(childNode) {
  delete childNode[OWNERSHIP];
}

// patch globals
const [
  { prototype: E },
  { prototype: N },
  { prototype: PF },
  { prototype: CD },
] = [
  Element,
  Node,
  PersistentFragment,
  getPrototypeOf(Text),
];

for (const key of Reflect.ownKeys(PF)) {
  if (key === 'constructor') continue;
  const { [key]: value } = PF;
  if (typeof value !== 'function') continue;
  for (const proto of [N, E]) {
    if (hasOwn(proto, key)) {
      const native = proto[key];
      defineProperty(proto, key, {
        value(...args) {
          return native.apply(this, args.map(asValueOf));
        }
      });
    }
  }
}

for (const key of ['after', 'before']) {
  for (const proto of [CD, E]) {
    const native = proto[key];
    defineProperty(proto, key, {
      value(...args) {
        return native.apply(this, args.map(asValueOf));
      }
    });
  }
}
```

**index.html**
```html
<!doctype html>
<script type="module">
  import PersistentFragment from "./persistent-fragment.js";

  const text = value => document.createTextNode(value);
  const a = text('a');
  const c = text('c');
  const pf = new PersistentFragment(a, c);
  const hr = document.createElement('hr');

  document.body.append(pf, hr);
  setTimeout(
    () => {
      pf.insertBefore(text('b'), c);
      setTimeout(
        () => {
          hr.after(pf);
        },
        1000
      );
    },
    1000
  );
</script>
```

You would see `ac`, then `abc`, both before the `hr`, then you'll see the fragment moved *after* that `hr` with `abc` as resulting DOM content.

I think this simplification is superior to my original proposal as it answers tons of questions and simplifies everything that needs simplification around this topic and I believe it would be an awesome feature to have for any library author out there.

-- 
Reply to this email directly or view it on GitHub:
https://github.com/whatwg/dom/issues/736#issuecomment-2476840911
You are receiving this because you are subscribed to this thread.

Message ID: <whatwg/dom/issues/736/2476840911@github.com>

Received on Thursday, 14 November 2024 16:17:13 UTC