-
Notifications
You must be signed in to change notification settings - Fork 308
Proposal: a DocumentFragment whose nodes do not get removed once inserted #736
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
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 |
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) Now I'll wait for any outcome 👋 |
@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. |
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. |
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 |
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 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 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. |
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. |
Nothing is kept in the DOM. It's a fragment that acts like a fragment. |
So, I've uploaded the previously mentioned polyfill, which should have 100% code coverage. There is a live test page too. the whatThe 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, The DPF (in short) has only one extra method, compared to DocumentFragment, which is 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 ownershipIt 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 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 summaryThe 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. |
@rniwa FYI I've filed the bug that makes current polyfill edit I've fixed the current polyfill with a workaround after a feature detection, so this can work on Safari/WebKit too 👋 |
Then what you created is indistinguishable from |
@rniwa |
It's nothing to do with Shadow DOM.
In the latest iterations of the proposal @justinfagnani at Google and we're working on, NoteTemplatePart is a thing that could be used without any template although we probably need to rename it to something else. |
@rniwa I've no idea what is this It's a document-fragment at all effects, it's indeed inheriting the same constructor, but it works transparently and only if the owner/creator keeps a reference around. If this is exactly what this Thanks. |
would you mind amending/canceling that comment since nothing in there is relevant to this proposal, so that people don't get distracted by concerns that are not part of this proposal? links to the solutions previously discussed would be more than welcome too. Thanks. |
The context you're missing is https://github.com/w3c/webcomponents/blob/gh-pages/proposals/Template-Instantiation.md and some F2F discussion (probably somewhere in minutes linked from issues in that repository) that encouraged making the parts there true primitives instead of tightly coupled with templating. (Also, please try to consolidate your replies a bit. Each new comment triggers a new notification for some and there's over a hundred people watching this repository. When in doubt, edit an existing reply.) There's another meeting coming up, and I hope @justinfagnani and @rniwa can make the current iteration a bit more concrete by then, as referencing it in this issue as if it's a thing everyone should be aware of is a lil weird. |
Thank you, I'm glad I read the second part of that comment about true primitives. I had read that proposal before and admittedly a little terrified that the DOM spec would include such an opinionated implementation. That spec reads like designing a render framework. I mean I'm sure we could do worse, but I'm encouraged to know that simpler proposals are under consideration. What I like about this proposal is the transparent use with Node API's appendChild/removeChild since it can be treated interchangeably with actual DOM nodes when say returned from some sort of Template instantiation. I think ownership is the challenge with a lot of value comes from having a clear single ancestor(whether single node or fragment). It lets JS context be tied to specific parent nodes without making more elements in the DOM. But by their very nature I don't see how you'd avoid nesting. Like a loop over these fragments with a top level conditional in it that also returns a fragment. In the end the DOM structure would be pretty flat but you'd have persistent fragments in persistent fragments. Since they aren't part of the actual rendered tree it becomes harder to understand what falls under each since nested dynamic content in this case could change what is in the top fragment. I would absolutely love to see a solution in this space having spent a lot of time figuring out tricks to accomplish similar things. Everyone writing a render library where they don't virtualize the tree hits this issue sooner or later. |
which is fine, and since appending a fragment to itself breaks, there's nothing different on the DOM. Current implementation / proposal allows shared nodes between fragments, which is the same as creating a fragment on the fly and append any node found in the wild: nothing stops your from doing that, everything works, no error thrown. The current idea is that keeping it simple is the only way to quickly move forward, while any ownership concept would require bigger, non backward compatible, changes. Libraries and frameworks authors won't worry about that anyway, 'cause they are the one creating nodes too, and they are those virtualizing thee in trees. Having a mechanism to move N nodes at once, accordingly with any DPF appended live, it's also a feature that would simplify state-machine driven UIs. Last, but not least, hyperHTML has this primitive since long time and it works already, but it doesn't play super nice with the DOM if thrown there as is, and it requires special treatment when used right away. This proposal would cover that case can much more. |
Actually CData sections and processing instruction nodes are simply completely broken in |
Is there any continued interest in this proposal? Would a persistent fragment have access to some DOM methods like |
I like this proposal a lot more after sitting with it a bit longer. At first I was thinking this was about library code managing the moving and managing of ranges of elements, but this is more. This helps with giving the users of said libraries the equivalent of React JSX Fragments. Like consider: const div = html`<div></div>`
const frag = html`<div></div><div></div>`
// further down
const view = html`<div>${ condition ? div : frag }</div>` I had a user ask why the div always worked, but the second time they attached the fragment why did it not render anything. The answer of course was that In a library based on KVO Observables top level dynamic updates that execute independently of top down reconciliation having a standardized DOM API that works the same whether attached or not is hugely helpful in this scenarios. Beyond that all these libraries have a similar concept. Being just a DOM node works very consistently with the whole // tagged templates
const el = html`______` //or
// jsx
const el = <______ /> way of approaching rendering which has been gaining steam (looking at the API's on the top end of performance in the JS Frameworks Benchmark). More and more non-virtual DOM libraries are picking this approach and showing it is performant. |
AFAIK there's not such thing and it would be useful only for SSR purposes where there is no DOM so I am not sure this is WHATWG responsibility but I am sure internally every engine has some sort of |
Btw, I just wanted to say I already use That's all I wanted to say. |
what do you mean? if I write this content (and we should keep in mind hydration is part of this story) I have all sort of unexpected things on the living DOM: <!doctype html>
<html lang="en">
<head>
<group>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</group>
<group><title>shenanigans</title></group>
</head>
<body>
<ul>
<li>1</li>
<group><li>2</li></group>
<li>3</li>
</ul>
<select>
<option>1</option>
<group><option>2</option></group>
<option>3</option>
</select>
<table>
<tr>
<td>1</td>
<group><td>2</td></group>
<td>3</td>
</tr>
</table>
</body>
</html> Result as image: To sum it up:
but most importantly, somebody complained about the tree being unexpected when having a wrapper node makes everything even more unexpected from tree walking perspective ... and then again, I would be all up for a "transparent node" that works as nodegroup container without breaking/moving/shifting elements while parsing to fix the unexpected but that seems like changing everything the DOM (or its parsing) knows to date so I doubt it's a viable solution unless we want to wait years before having something usable ... wouldn't you agree? edit the table as I see it in devtools: <table>
<tbody>
<tr>
<td>1</td>
<td>2</td>
<td>3</td>
</tr>
</tbody>
</table> |
group {
display: contents;
} HTML<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<div style="display: contents">
<title>Test: Div Wrapped Elements</title>
</div>
</head>
<body>
<h1>Testing HTML Elements with <div style="display: contents"></h1>
<!-- Option inside select with div -->
<h2>Option inside Select</h2>
<select>
<option>Option 1</option>
<div style="display: contents">
<option>Option 2 (inside div)</option>
</div>
</select>
<!-- List items inside div -->
<h2>List Items inside UL</h2>
<ul>
<li>Item 1</li>
<div style="display: contents">
<li>Item 2 (inside div)</li>
</div>
</ul>
<!-- Table with div-wrapped tr and td -->
<h2>Table with Div-Wrapped TR/TD</h2>
<table border="1">
<thead>
<tr><th>Header</th></tr>
</thead>
<tbody>
<div style="display: contents">
<tr>
<div style="display: contents">
<td>Cell 1 (wrapped in divs)</td>
</div>
</tr>
</div>
</tbody>
</table>
<!-- Picture element with real images -->
<h2>Picture Element with Source</h2>
<picture>
<div style="display: contents">
<source srcset="https://picsum.photos/300/200.webp" type="image/webp">
</div>
<img alt="Fallback Image" width="300">
</picture>
<!-- Video element with source from Google CDN -->
<h2>Video Element with Source</h2>
<video controls width="300">
<div style="display: contents">
<source src="https://storage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4" type="video/mp4">
</div>
Your browser does not support the video tag.
</video>
</body>
</html> But:
So: DOM JS for tableconst table = document.createElement("table");
table.border = "1";
const thead = table.createTHead();
const headerRow = thead.insertRow();
headerRow.insertCell().outerHTML = "<th>Header</th>";
const tbody = table.createTBody();
const divTR = document.createElement("div");
divTR.style.display = "contents";
const tr = document.createElement("tr");
const divTD = document.createElement("div");
divTD.style.display = "contents";
const td = document.createElement("td");
td.textContent = "Cell 1 (inside divs)";
divTD.append(td);
tr.append(divTD);
divTR.append(tr);
tbody.append(divTR);
document.body.append(table); |
@DeepDoge that doesn't work if served as such from the server ... that's my point, it breaks the hydration story, it's not a solution to me, happy to learn it works for your case but personally I wouldn't abuse the DOM that way and all 3rd party library expectations around a clean tree are gone. The idea here is to never need hacks and find a standard way forward that works for SSR too. edit ... "what doesn't work" ?
|
@WebReflection I was just saying, it wasn't a problem with all the things you listed.
Also, I just seen it more of small change by introducing a new primitive like new attribute that makes the element transparent, which also might be useful for other use cases other than "fragments" as well. Instead of engineering something new. I never said it should be that in the whole page. From the start I was just saying that I don't like that |
that's what happens with fragments though ... its content Then again, I am waiting for @rniwa or whoever else to work on, or present, a PoC that satisfies the requirements and I would not complain if that's an attribute, a |
That's probably the difference, I'm more interested it's being in the DOM rather than being something abstract. More verbose. When I see it on the DOM I wanted to say, OK this is a wrapper Like when you are listening the DOM with
Doesn't Anyway, either way, as long as we have something useful, I don't really care what it's or how it works. |
you can have NodeGroup in NodeGroup and the top level one is not necessarily connected ... the parent in that scenario (nested non live node groups) becomes even more cumbersome ... right now the parent is either the current one where the group was inserted, or
the current prototype/idea is that anonymous groups don't share their reference so there is an ownership concept same way if you create a fragment and you don't leak it nobody can interfere with its logic (assuming is a class extends that does more and want to control or change or track or do whatever with its children when it's been created offline). If it becomes something everyone can mess around with this simple contract won't exist anymore and personally I like that simple contract but, like you said:
|
WebReflection/group-nodes#12 TL;DRRecently, I brought this idea to address concerns about multi-parenting and unclear traversal strategies. But as @WebReflection mentioned the idea is not about having a transparent container, but having a reference point in a document to manage nodes without risk of being altered by other scripts unless it is explicitly shared. Thus @WebReflection suggests that concealing the children from being discovered from outside1 by Tree Walking (i.e. 1 Though @WebReflection also suggests to conceal a reference point (i.e. @WebReflection Please tell me if I got something wrong 🙏 |
I was originally pretty sceptical of the idea of a wrapper element that appears as a true parent in the DOM tree Ex: <fragment src="/content.html">
<span class="spinner"></span>
Loading...
</fragment> Fragments children would be replaced once loaded, and if src isn't provided then it is a simple way to group nodes without needing to special case CSS selectors with all the benefits discussed in this proposal. One added benefit of this approach is that event listeners can be attached to the fragment which would allow event delegation for it's children.
A wrapping element like this is also impossible to break, where as marker based fragments can be broken when a marker is moved away from it's partner (ex: extracting a range of content that starts in the middle of a fragment and ends outside). It's also possible that similar to providing a solution for includes in the future, it could help solve out of order streaming without need of JS. |
@robbiespeed about this:
does this happens only at the rendering level or would that at the polyfill level, a good orchestration of MutationObserver to track fragments parents (target) around should do ... but I'd like to be sure everyone is OK with this way of shipping fragments (@rniwa and/or others) so that it won't be a waste of (personal, unfortunately almost non-existent) time. |
@WebReflection that's a good question, I expect that the A full example for those not following this thread closelyul > li {
color: purple;
:first-child {
color: green;
}
} <ul>
<fragment src="/items.html">
<li>Loading...</li>
</fragment>
<li>Last</li>
</ul> When Once <ul>
<fragment src="/items.html">
<li>First</li>
<li>Second</li>
</fragment>
<li>Last</li>
</ul> Green "First" item, purple "Second" and "Last" items. From the perspective of CSS |
I was thinking about fallbacks for fragment boundaries, I think this way it should solve this problem. <template shadowRootMode="closed" as="group" name="myGroupName">
<p>Text</p>
<span>123</span>
</template> This way we follow the rule proposed by @WebReflection (#736 (comment)), which is hiding the internal structure, while making a way for serialization since template is already there way some time, so it should be invisible for older versions of DOM. For newer versions that implement groups, it would actually create a group node. Alternative <template shadowRootMode="closed" group="myGroupName">
<p>Text</p>
<span>123</span>
</template> But my choice the one above as I also consider an element that would be inert (as a separate proposal), while either assignable with an element or attachable with |
@robbiespeed if that is:
I would be sold without second thoughts ... the reason I never used that approach is that I've thought it was not possible/desired to create such element and I think the This should also make Justin happy as that would reflect his idea too, if I am not mistaken. @FrameMuse let's please not go down that SD road, what @robbiespeed is proposing is exactly what we all wanted, including yourself IIRC ... it's true that |
@WebReflection Then we need very flexible and minimal primitive than what we have now or it's going to be ignored very likely to my opinion. |
To be clear I don't think Html includes would be great, as would persistent fragments. For includes to work though I think it requires something like
|
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 |
@rniwa can you be more specific about what kinds of changes to parser behaviour is problematic? Template is a wildly different scenario, since it's children are placed into a From a backwards compatibility scenario it would end up a |
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. |
@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 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. |
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
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 |
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? |
@robbiespeed That’s why having However, if the custom attributes proposal lands, that might let us bind behavior to elements regardless of whether they're 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 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 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. |
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 I don't have too much to say against it, please help fight this ^_^ |
I, personally, don't need/care about connected/disconnected events and I feel like it's entirely out of scope, even if proposed as 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 🙏 |
True, totally get it. I will just be waiting for something to land. |
@WebReflection @rniwa Ok, I agree, what should be our next steps? |
Uh oh!
There was an error while loading. Please reload this page.
edit I wouldn't mind
Node.DOCUMENT_PERSISTENT_FRAGMENT_NODE
name/kind neitherTL;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:
<TD>
or<TR>
through such fragment, and keep a reference for future updatesWhy
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
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.
Possible F.A.Q. Answers
childNodes
as immutable, as it is for regular fragmentsvalueOf()
, thereferences
part can be ignoredThanks in advance for eventually considering this, happy to answer any possible question.
The text was updated successfully, but these errors were encountered: