Repository navigation
Description
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]; // trueThere 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
childNodesas 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(), thereferencespart can be ignored
Thanks in advance for eventually considering this, happy to answer any possible question.
Activity
WebReflection commentedon Mar 6, 2019
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
WebReflection commentedon Mar 6, 2019
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 👋
[-]Proposal: Node.DOCUMENT_LIVE_FRAGMENT_NODE[/-][+]Proposal: a DocumentFragment whose nodes do not get removed onse inserted[/+]WebReflection commentedon Mar 7, 2019
@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.
[-]Proposal: a DocumentFragment whose nodes do not get removed onse inserted[/-][+]Proposal: a DocumentFragment whose nodes do not get removed once inserted[/+]annevk commentedon Mar 7, 2019
Do https://whatwg.org/working-mode#changes and https://whatwg.org/faq#adding-new-features help?
I suspect we'll need something like this to build templating on top of.
cc @rniwa @justinfagnani @wycats
developit commentedon Mar 7, 2019
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
justinfagnani commentedon Mar 7, 2019
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
Partinterface is essentially this, without being a Node living in the childNodes list.display: contentsalso 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
Nodesubclass reachable by tree-walking a document is serializable to HTML (including<template>and it's uniqueinnerHTMLimplementation). 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.
rniwa commentedon Mar 10, 2019
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.
WebReflection commentedon Mar 10, 2019
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.
WebReflection commentedon Mar 11, 2019
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.previousSiblingwould return thedpf.firstChild.previousSiblingif there is afirstChild. If thedpf.isConnectedisfalse, the result isnull.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
WeakMapthat 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.
WebReflection commentedon Mar 11, 2019
@rniwa FYI I've filed the bug that makes current polyfill
not usable* in WebKit/Safariedit I've fixed the current polyfill with a workaround after a feature detection, so this can work on Safari/WebKit too 👋
165 remaining items
rniwa commentedon May 7, 2025
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.
robbiespeed commentedon May 7, 2025
@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
fragmentelement 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.
WebReflection commentedon May 7, 2025
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.
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.
not really ... it will impact bad code and libraries but those already negatively affect users ... I mean, some library still use
innerHTMLinstead of preserving nodes identities, this is not a Web platform concern as long as specs are clear.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.FrameMuse commentedon May 7, 2025
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?
nomadshiba commentedon May 7, 2025
@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 adoptedCSSStyleSheet, 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 anHTMLElement. But since you mentioned it wouldn't be anHTMLElement, but rather anotherElementtype, 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
HTMLElementor not—assuming they work on anyElementtype.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
connectedCallbackanddisconnectedCallbackare crucial. If the fragment never needed to change, a one-timeDocumentFragmentor even an array ofChildNodereferences 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
Nodetype 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.
FrameMuse commentedon May 8, 2025
Indeed, but what about scripts/libraries that don't know you're using a new element?
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 ^_^
WebReflection commentedon May 8, 2025
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 🙏
nomadshiba commentedon May 8, 2025
True, totally get it. I will just be waiting for something to land.
FrameMuse commentedon May 9, 2025
@WebReflection @rniwa Ok, I agree, what should be our next steps?
FrameMuse commentedon Jun 19, 2025
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
NodeGroupin 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
NodeGroupnaming butGroupto avoid problems in future if this gets implementedHere'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.
WebReflection commentedon Jun 19, 2025
you lost me in the follow up ...
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 ...
FrameMuse commentedon Jun 19, 2025
Sorry I misspelled it. It's NOT based on DOM patching.
WebReflection commentedon Jun 19, 2025
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 👍
FrameMuse commentedon Jun 19, 2025
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.
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.
jonathandewitt-dev commentedon Mar 31, 2026
to be fair, the template element was one of the biggest game changers in the modern web, imho. So worth the trouble.