Skip to content

Proposal: a DocumentFragment whose nodes do not get removed once inserted #736

Open

Description

@WebReflection

edit I wouldn't mind Node.DOCUMENT_PERSISTENT_FRAGMENT_NODE name/kind neither

TL;DR

The document fragment is a great primitive to wrap together numerous nodes and append these directly as batch, however a fragment loses all its children as soon as appended, making it's one-off usage limited in those cases where a list of nodes, at a certain position, is meant.

This proposal would like to explore the possibility of a live document fragment that:

  • does not lose its childNodes once appended
  • it's still transparent / unaddressable from CSS
  • it's also transparent for any other node so that it's still possible, as example, to append many <TD> or <TR> through such fragment, and keep a reference for future updates

Why

Both virtual DOM based libraries, such React, as well as direct DOM based one, such as hyperHTML, or lit-html, have been implementing their own version of a persistent fragment in a way or another.

If there was a primitive to directly reference more than a node, through a fragment with a well known position on the DOM, I am pretty sure all libraries would eventually move to adopt such primitive, so that a tag function could handle both <p>1</p> and <p>1</p><p>2</p> without needing to re-invent a similar wheel every single time, and making portability between libraries and frameworks easier than ever: it's just a DOM node!

Example

// either persistent or live
const pf = document.createLiveFragment();

// append zero, one, or any amount of nodes
pf.appendChild(document.createElement('TD'));
pf.appendChild(document.createElement('TD'));

// make it live
const lastTr = document.querySelector('#data tr:last-child');
lastTr.appendChild(pf);

// references are still there
pf.childNodes.length; // 2

// the node is invisible though
lastTr.lastChild === lastTr; // false
lastTr.lastChild === lastTr.childNodes[1]; // true

There should be no way to interfere with CSS and/or selectors, the fragment is either referenced somewhere else or it won't exist for the DOM.

How

The way hyper/lit-html are doing this is by abusing comment nodes as boundaries of these virtual fragments. The itchy part of these libraries is mostly represented by these virtual fragments, 'cause it's obvious if the primitive proposed here would exists, these libraries would've used it instead (happy to be corrected, but at least I would never create my own virtual fragment if I could use something else).

The way this could be implemented, is by weakly referencing nodes to such fragment only if this is held in memory.

// example implementation of the live fragment
const references = new WeakMap;
class LiveFragment extends DocumentFragment {
  #childNodes = [];
  #appendChild = node => {
    this.#removeChild(node);
    this.#childNodes.push(node);
  };
  #removeChild = node => {
    const i = this.#childNodes.indexOf(node);
    if (-1 < i)
      this.#childNodes.splice(i, 1);
  };
  appendChild(node) {
    this.#appendChild(node);
    return super.appendChild(reference.call(this, node));
  }
  append(...nodes) {
    nodes.forEach(this.#appendChild);
    return super.append(...nodes.map(reference, this));
  }
  removeChild(node) {
    this.#removeChild(node);
    references.delete(node);
    return super.removeChild(node);
  }
  // the value of this LiveFragment when moved around
  valueOf() {
    this.append(...this.#childNodes);
    return this;
  }
}
function reference(node) {
  references.set(node, this);
  return node;
}

// amend on the appendChild standard
const {appendChild} = Node.prototype;
Node.prototype.appendChild = function (node) {
  appendChild.call(this, asFragment(node));
};

const {append} = Element.prototype;
Element.prototype.append = function (...nodes) {
  append.apply(this, nodes.map(asFragment));
};

function asFragment(node) {
  return node instanceof LiveFragment ?
    // it could be just node.valueOf() for everything
    // explicit here for explanation sake
    node.valueOf() :
    node;
}

Possible F.A.Q. Answers

  • if a node is manually appended somewhere it's fine. But as soon as the LiveFragment owner will append such fragment again, that node would be moved back (nodes ownership by creator)
  • if the LiveFragment has no references, then nothing changes from a fragment
  • a live fragment always exposes its childNodes as immutable, as it is for regular fragments
  • everything is the same, except that live fragments creators, owner of their live fragment content, can use this primitive instead of polluting the DOM with comments
  • it is always possible for DOM engines to know if a node belong to a fragment, as long as this is still referenced somewhere. If unnecessary, due forced re-append on valueOf(), the references part can be ignored

Thanks in advance for eventually considering this, happy to answer any possible question.

Activity

  1. WebReflection commented on Mar 6, 2019

    @WebReflection
    Author

    FWIW this is apparently a very demanded feature from the Web Developers community

    Previously on a similar proposal: https://discourse.wicg.io/t/proposal-live-fragments/2507

  2. WebReflection commented on Mar 6, 2019

    @WebReflection
    Author

    As simplified approach, and for demo sake, I'll leave a playground that works out of the box in Chrome Canary (but not yet in Code Pen, however I've informed them about it)
    https://codepen.io/WebReflection/pen/moRQRV?editors=0010

    Now I'll wait for any outcome 👋

  3. transferred this issue fromwhatwg/htmlon Mar 7, 2019
  4. changed the title [-]Proposal: Node.DOCUMENT_LIVE_FRAGMENT_NODE[/-] [+]Proposal: a DocumentFragment whose nodes do not get removed onse inserted[/+] on Mar 7, 2019
  5. WebReflection commented on Mar 7, 2019

    @WebReflection
    Author

    @annevk just FYI I think there's a typo in the title: onse => once

    also, if I might ask, what does the label "needs implementer interest" mean?

    What can I do to move this forward? Should it be me the implementer?

    Thanks.

  6. changed the title [-]Proposal: a DocumentFragment whose nodes do not get removed onse inserted[/-] [+]Proposal: a DocumentFragment whose nodes do not get removed once inserted[/+] on Mar 7, 2019
  7. annevk commented on Mar 7, 2019

    @annevk
    Member
  8. developit commented on Mar 7, 2019

    @developit

    This is interesting. Other names I've seen used to casually refer to something along these lines are "Persistent Fragment" (given that it doesn't empty when appended) and just "Range Fragment".

    Example: https://discourse.wicg.io/t/proposal-fragments/2312

  9. justinfagnani commented on Mar 7, 2019

    @justinfagnani

    Edit: I didn't see that in this version of the idea the fragment doesn't live in the tree. I think that makes it functionally equivalent to NodePart from TemplateInstantiation, or a wrapper around StaticRange, and addresses most of the issue below, which were based on other variations of the idea I'm familiar with.

    @annevk I think the Template Instantiation Part interface is essentially this, without being a Node living in the childNodes list. display: contents also covers some similar use cases.

    One concern with a new node type is that much existing tree traversal code will not know how to handle it, so a live fragment and its children will likely be skipped. Depending on where such live fragments are intended to be used this may or may not be a problem. There is a lot of code out there that assumes that only Elements can contain other Elements.

    Another concern is that right now I believe that ever Node subclass reachable by tree-walking a document is serializable to HTML (including <template> and it's unique innerHTML implementation). The fact that DocumentFragment cannot be placed into a childNodes list preserves this. I'm not sure how much this matters, but this would be the first node type in the tree (but not tree-of-trees) that can't survive a serialize/parse round-trip.

    We'd also have to consider how other APIs work. Do live fragments show up on event.path? Can children of a live fragment be slotted into the fragment's parent's ShadowRoot? etc...

    I'm not sure if the issues are insurmountable, but I've been working on Template Instantiation with the theory that the least disruption will be caused with a new non-Node interface that lives outside the tree, like Range. Then all existing tree processing code will work as-is.

  10. rniwa commented on Mar 10, 2019

    @rniwa

    Yeah, we've definitely considered this approach before making the template instantiation proposal but fundamentally, all we need is tracking where the inserted contents need to be. I don't think there is any reason to create a new node type and keep it in the DOM if we can avoid it.

  11. WebReflection commented on Mar 10, 2019

    @WebReflection
    Author

    Nothing is kept in the DOM. It's a fragment that acts like a fragment.
    I'm working on a better playground that shows the idea fully polyfilled so please wait for it to be online and evaluable before closing this, thanks.

  12. WebReflection commented on Mar 11, 2019

    @WebReflection
    Author

    So, I've uploaded the previously mentioned polyfill, which should have 100% code coverage.
    https://github.com/WebReflection/document-persistent-fragment

    There is a live test page too.

    the what

    The idea is to have a 1:1 DocumentFragment alter-ego that doesn't lose its nodes.

    The fragment is exposed only to the owner, so that there is no way to retrieve it from third parts, unless passed around, and there's nothing live on the DOM, if not, eventually its child nodes.

    The proposal exposes to the owner common nodes methods based on its content.

    As example, dpf.previousSibling would return the dpf.firstChild.previousSibling if there is a firstChild. If the dpf.isConnected is false, the result is null.

    The DPF (in short) has only one extra method, compared to DocumentFragment, which is dpf.remove(). Such method doesn't need much explanation: it removes all nodes from the DOM.

    All operations performed through the DPF are reflected live on the document, and while this might be just a stretch goal, it is super easy and nice to simply update an owned reference and see everything changing live.

    Nodes ownership

    It is possible to grab random nodes and destroy these, or change these, affecting indirectly the content owned by the DPF instance, but it's always been possible to be obtrusive on the DOM and destroy third parts libraries so I think this shouldn't be concern.

    However, I could implement the WeakMap that relate each node to a specific DPF instance in a way that it's not possible to move a node owned by a DPF into another DPF, or even perform any action on the DOM through elements that are not the DPF owner.

    This, however, would introduce an ownership concept that is too different from what we've used so far, but I believe this proposal is for all libraries that need such persistent fragment, and that owns their own nodes, libraries that are currently somehow already breaking things if 3rd parts obtrusive libraries destroy, or manipulate, DOM nodes in the wild.

    As summary

    The fact, beside some Safari glitch I'm sure I can solve, this whole proposal can be already polyfilled, and the fact browsers have a way to optimize it and make it blazing fast, should be considered as a plus, 'cause instantly adoptable by the community, so that we can have quick feedbacks of how much this is welcomed or needed out there.

    Please don't hesitate to file issues there or ask me more here before discarding this proposal.

    Thank You.

  13. WebReflection commented on Mar 11, 2019

    @WebReflection
    Author

    @rniwa FYI I've filed the bug that makes current polyfill not usable * in WebKit/Safari

    edit I've fixed the current polyfill with a workaround after a feature detection, so this can work on Safari/WebKit too 👋

  14. 165 remaining items

  15. rniwa commented on May 7, 2025

    @rniwa

    @rniwa can you be more specific about what kinds of changes to parser behaviour is problematic?

    Any kind of HTML parser change is problematic. We don't want to make any changes to the HTML parser at all at this point unless there is an extremely compelling reason to do so. Given there are alternatives to support this feature without affecting HTML parser changes, I don't see how we'd pick the alternatives that do.

  16. robbiespeed commented on May 7, 2025

    @robbiespeed

    @rniwa Having such limits on parser changes sounds like a major problem for advancing HTML, are there any proposals to relax/generalise the parsing rules such that future features are not such a problem to add?

    As far as I can tell the XML parser used for XHTML would have no issues, but I did take a look at the HTML parser spec and it is significantly complex, so I see why changes are avoided. If there's no path forward for improving the HTML parser situation maybe it makes sense for the web to push for an XHTML future once again.

    I do still think a fragment element would be worth it considering that HTML includes would need similar spec changes, and there's something to be gained from aligning the features together.

    The comment marker based approach for grouping is also not without it's downsides, it's possible for groups to break due to marker pairs being separated in the tree. That's going to impact users, and will also need to be addressed in the DOM spec.

  17. WebReflection commented on May 7, 2025

    @WebReflection
    Author

    If there's no path forward for improving the HTML parser situation maybe it makes sense for the web to push for an XHTML future once again.

    I'd welcome that but right now HTML5 and XHTML are not fully interoperable (last time I've checked) so that would be a new world of potential incompatibilities between browsers, SSR and whatnot.

    it's possible for groups to break due to marker pairs being separated in the tree

    not in my prototype ... those markers fail fast in that scenario so if you break the contract the fragment is broken, as easy as that. At the parsing level, if those markers are messed up no group exists ... still KISS and working with all primitives we have.

    That's going to impact users

    not really ... it will impact bad code and libraries but those already negatively affect users ... I mean, some library still use innerHTML instead of preserving nodes identities, this is not a Web platform concern as long as specs are clear.

    will also need to be addressed in the DOM spec

    true that, but the contract is very simple: https://github.com/WebReflection/group-nodes/blob/main/src/lib/utils.js#L74-L88

    I would still welcome a <fragment> element but that's why I've asked for @rniwa consensus (or others) because I knew before proposing the prototype that would've not been welcomed.

  18. FrameMuse commented on May 7, 2025

    @FrameMuse

    I welcome any solution actually at least to try it out under experiments, but for now I see this proposal as non-serializable existing only as in-memory structure, this would still fit to my use-case, later on we can extend it to be serializable if possible.

    So far, we have several cool features beyond just Persistent Fragment and more-less basic primitive (I still think we should find even more basic one), I think we should try to discuss it a bit more, demonstrate very basic use-cases, how this naturally fits to @rniwa DOMParts proposal and then frame it and probably create a PR. What do you think guys, is it the time?

  19. nomadshiba commented on May 7, 2025

    @nomadshiba

    @robbiespeed
    What you did with [src] can already be done using custom elements, which also allow for more flexible logic around how the content gets filled. The issue is, that it wouldn't be transparent. You might try applying :host { display: contents } via an adopted CSSStyleSheet, but as @WebReflection pointed out, that comes with its own trade-offs.

    That’s why having <fragment> as a standalone, purpose-built element makes more sense—similar to how elements like <canvas>, <iframe>, <video>, or <img> exist for specific use cases. You could still build on top of it using custom elements to add your own logic—if it were an HTMLElement. But since you mentioned it wouldn't be an HTMLElement, but rather another Element type, it couldn’t be used as a custom element.

    However, if the custom attributes proposal lands, that might let us bind behavior to elements regardless of whether they're HTMLElement or not—assuming they work on any Element type.

    Now, why does lifecycle even matter in this context? One common reason people want a persistent fragment is to update or move nodes around as a group. If updates are involved, you'll probably need to react to some kind of signal/event—and that means you need clean up that listener when the fragment is removed from the DOM. That’s why lifecycle callbacks like connectedCallback and disconnectedCallback are crucial. If the fragment never needed to change, a one-time DocumentFragment or even an array of ChildNode references would have been enough.

    Custom elements (and maybe custom attributes in the future) already provide a standardized way to handle lifecycles, so introducing a completely new system just for new fragment/group Node type might be redundant. Or maybe it's just fine.

    To be clear, I’m not attached to a specific solution—I just want us to get something. My concern is that whatever we end up with might not be flexible or lifecycle-aware enough, and that would make it barely better than managing a raw array of nodes—still fragile when DOM changes since it would relay on frameworks' hacky abstract ways of handling lifecycles.

    As for concerns about breaking existing tree walkers: I don’t find that argument very compelling. If a developer chooses to use a new element or node type, it's on them to handle it properly.

  20. FrameMuse commented on May 8, 2025

    @FrameMuse

    As for concerns about breaking existing tree walkers: I don’t find that argument very compelling. If a developer chooses to use a new element or node type, it's on them to handle it properly.

    Indeed, but what about scripts/libraries that don't know you're using a new element?


    Now, why does lifecycle even matter in this context? One common reason people want a persistent fragment is to update or move nodes around as a group. If updates are involved, you'll probably need to react to some kind of signal/event—and that means you need clean up that listener when the fragment is removed from the DOM. That’s why lifecycle callbacks like connectedCallback and disconnectedCallback are crucial. If the fragment never needed to change, a one-time DocumentFragment or even an array of ChildNode references would have been enough.

    I agree, I like and want connection events to be observable, but the concern here is use-cases like
    "you as a developer do your code and you already specified where and how the nodes are attached/removed, so why you need events if you already "know" that?" or "we already have libraries..."

    I don't have too much to say against it, please help fight this ^_^

  21. WebReflection commented on May 8, 2025

    @WebReflection
    Author

    I, personally, don't need/care about connected/disconnected events and I feel like it's entirely out of scope, even if proposed as <fragment>, 'cause it shouldn't be different from any other element that doesn't natively have those hooks backed in.

    All these discussions will only delay more and more this proposal which is kinda sad ... we all said "whatever it is would be better than nothing" but then we all have opinions that apparently can only cause friction for this proposal to move forward.

    I think @rniwa nailed it with the fact there is already an easy to implement solution so that we shouldn't keep asking stuff that is not strictly part of the proposal or a requirement when it comes to have anyway a node that can be diffed/moved/removed purposes?

    The result is likely that nothing will ever land due bikeshedding/features-creep scenarios not meant or needed to start with, imho, so let's please try to stop asking for new things and focus on what's actually best for the current proposal to move forward?

    It's a primitive, it should do one thing, we can iterate on more things after such primitive landed, right?

    Thanks in advance for your understanding 🙏

  22. nomadshiba commented on May 8, 2025

    @nomadshiba

    we can iterate on more things after such primitive landed, right?

    True, totally get it. I will just be waiting for something to land.

  23. FrameMuse commented on May 9, 2025

    @FrameMuse

    @WebReflection @rniwa Ok, I agree, what should be our next steps?

  24. FrameMuse commented on Jun 19, 2025

    @FrameMuse

    After struggling several months spent brainstorming how I could implement this without patching DOM, something which seemed to be impossible turned out to be pretty easy.

    Not everything, but essential parts like "remote attaching" of nodes and "PersistentFragment" was achieved purely in conventional VanillaJS.

    I'm not sure if anyone did this before me, so anyway I'm sharing what I did and how did it:
    https://github.com/framemuse/node-group

    It implements NodeGroup in the way I'm personally using it, other use-cases could be implemented in future, now I'm into them, so I left them unimplemented. Currently, it's pretty stable and as it's NOT based on DOM patching - I'm using this production environment, so far good enough.

    It's not using NodeGroup naming but Group to avoid problems in future if this gets implemented

    Here's the tests & playground:
    https://stackblitz.com/edit/node-group

    It 100% fulfills the first proposal here, which is DocumentPersistentFragment, which is consecutive appending of a fragment, so you don't have to append nodes to a fragment each time you use it. Other things are a bit less stable, but still working predictable enough.


    Despite this (imho pretty successful) implementation, I still want to see something like this being a Web Standard and this is yet another step to prove this is needed.

    It's different from what @WebReflection has implemented, it's not using comments, it uses the aspects I came up with in this comment.

  25. WebReflection commented on Jun 19, 2025

    @WebReflection
    Author

    how I could implement this without patching DOM, something which seemed to be impossible turned out to be pretty easy.

    you lost me in the follow up ...

    it's pretty stable and as it's based on DOM patching

    this topic should not creep on implementation details bikeshedding, imho, it should move forward with OKs around vendors and interest to implement ... then we can discuss the details ...

  26. FrameMuse commented on Jun 19, 2025

    @FrameMuse

    this topic should not creep on implementation details bikeshedding, imho, it should move forward with OKs around vendors and interest to implement ... then we can discuss the details ...

    Sorry I misspelled it. It's NOT based on DOM patching.

  27. WebReflection commented on Jun 19, 2025

    @WebReflection
    Author

    great ... but just as reminder: the hydration story gotta be in ... no comments? no hydration, and that's a huge red flag to me, no matter how much you didn't patch the dom there (but again, if an append can survive fragments, you either missed the part where persistent fragments should never be containers or the part we need comments around or no hydration is possible).

    anyway, good to see we haven't forgotten about this ... hopefully others didn't forget too 👍

  28. FrameMuse commented on Jun 19, 2025

    @FrameMuse

    hydration story gotta be in ... no comments?

    Yes, there is a potential for hydration too, it's not explored by me so far since I as mention - for me it's NOT priority right now, so I haven't implemented that, but I wrote an idea in the README of https://github.com/framemuse/node-group.

    but again, if an append can survive fragments, you either missed the part where persistent fragments should never be containers or the part we need comments around or no hydration is possible

    I think I remember that, my goal was to NOT patch the DOM, so I can use this in production projects right now, or at least grow it to be production-ready (since I want this so bad).


    Maybe I'm missing something, but as I'm comparing https://github.com/framemuse/node-group and your very initial proposal, it looks like it's doing exactly what was stated without Comments and DOM patching.

  29. jonathandewitt-dev commented on Mar 31, 2026

    @jonathandewitt-dev

    I don't think there is much appetite for introducing a new element which changes the behavior of HTML parser. We had to fight against dozens upon dozens of security bugs and crashes when we introduced template element. I don't think we want to repeat saga, not to mention that changing HTML parser resulted in a bunch of XSS issues in websites because there are non-browser implementations of HTML parser (e.g. used for server side rendering, etc...).

    to be fair, the template element was one of the biggest game changers in the modern web, imho. So worth the trouble.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    addition/proposalNew features or enhancementsneeds implementer interestMoving the issue forward requires implementers to express interest

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions