`fragment` node type
viktor-yakubiv opened this issue · 10 comments
Initial checklist
- I read the support docs
- I read the contributing guide
- I agree to follow the code of conduct
- I searched issues and couldn’t find anything (or linked relevant results below)
Problem
It could be useful to have a special type for fragments.
A fragment node type could help some plugins which need to preserve some isolated data for itself or replace a node with an array of nodes. Imagine a component for this case, that results in multiple nodes and has no single 'root' element. The idea is similar to JSX's fragment.
The plugins I bear in mind:
-
include— another file is imported but it has multiple nodes. It can be done with asplice()like below:parent.children.splice(index, 1, ...loadedRoot.children)
but having a
fragmentnode enables storing additional information (original properties, actual file location etc) in the node for possible future use. -
componentsThere is no limitation for number of children for the
<template>element, i.e. a web component could yield multiple children instead itself. If a component gets replaced (like inrehype-componetsplugin), it might be useful to preserve old data for other plugins that work on data based on that plugin.
Solution
Having a following interface could be useful.
interface Fragment <: Parent {
type: "fragment"
}Alternatives
With rehype, nested root nodes effectively work as fragments. This was tested running the following example:
const tree = {
type: 'root',
children: [
{ type: 'text', value: 'outside nested root\n' },
{
type: 'element',
tagName: 'div',
children: [
{
type: 'root',
children: [{
type: 'element',
tagName: 'span',
children: [{ type: 'text', value: 'inside nested root' }],
}]}
]
}
],
}
const file = await rehype()
.data('settings', { fragment: true })
.stringify(tree)
console.log(String(file))This code does not throw and yields the following HTML:
outside nested root
<div><span>inside nested root</span></div>The proposal is actually made for a sanity in specific cases. I understand that this is not a part of HTML and is rather a common practice in frameworks like React, Vue etc. and could be rejected as well as #20. However, as it could be useful for some plugins and does not require a lot of effort in the implementation, I am bringing this to your consideration.
Hi!
Currently, root can be (and is) used for that.
root does not need whole (valid?) documents: fragments are fine.
An example in hast where it’s used as that, is at node.content (when node is a <template>).
An example in utilities, is that hastscript can build roots, but also accepts roots and uses their children.
That is particularly needed because hastscript can be used as JSX (see here, syntax-tree/xastscript#4 (comment))
Note though that, unlike your alternative example, the roots are “unwrapped” instead of injected.
Depending on what you want to do, you can probably use roots for that?
Hi Titus!
Thanks for your quick response. The root node would surely work for my case. I only wanted to bring some clarity into naming. It seems to be very confusing when you find more than one root in the tree, especially when it's not the actual root.
Also, the documentation states:
Root can be used as the root of a tree, or as a value of the
contentfield on a'template'Element, never as a child.
when it can be actually used as a child and this works smoothly. Would it be worth to clarify this bit of the documentation or you would find this more confusing for new comers?
See my note: “Note though that, unlike your alternative example, the roots are “unwrapped” instead of injected.”! So it is indeed important not to have it as a child as the docs say, but you can still use the stuff inside the root.
@wooorm, it took me a while to rewrite my case of example and understand what you actually recommended previously. Thank you 🙂
Depending on what you want to do, you can probably use
roots for that?
Root nodes do fit my needs but I need them to be children because of passing some extra information with them. See two examples below:
- a (pseudo) web component gets replaced with its template — the web component host becomes a fragment but still carries host's properties and slots data down to the subtree
- a slot gets substituted with it's contents — slot element becomes a fragment; it's needed to be able to pass the fragment (without any
slotattribute one more time)
I actually remove fragments from the tree after all processing's done but preserving fragments is useful for debugging such complex tree replacements. I don't know if they might be useful in the tree after plugin finishes work.
Does my example and context bring any different though to you about fragments and roots?
Hey again. Hmm, I don’t quite grasp exactly what you want to do with webcomponents and why in the code you link, so I might be missing things!
I need them to be children because of passing some extra information with them
What information do you want to track in the tree, exactly? 🤔
I actually remove fragments from the tree after all processing's done […]
It sounds like what you’re asking for is instead something that does persist in the tree. Something that other plugins and utilities can expect to encounter.
If you do want to temporarily store some data which you’ll clean in the end, you can are free to add your custom elements into the tree (e.g., mdast-util-to-hast, and a few other utilities, support raw nodes, which represent strings of HTML in an otherwise AST for HTML), or maybe use a Map<Element, ExtraInformation>, so you can store that extra information at some point, and retrieve it later?
Of interest might also be that template elements already support a .content field on them. Which contains the “fragment” as nodes that was inside them as a string. Maybe what you want can be done with a template element? Or also with a .content field?
Hey again. Hmm, I don’t quite grasp exactly what you want to do with webcomponents and why in the code you link, so I might be missing things!
I added the read-me to the repository that explains what it is, why and what it does. I also added a more detailed example consisting of more than one component, a page that assemblies them all, compiled result and the rehype configuration file.
I hope this could clarify my intentions.
In short, I am trying to achieve what React or Svelte does but only for HTML, without any client JavaScript. From the syntax perspective, it would be the most similar to Vue 3 (a bit less to Svelte). The declarative web components spec is good but didn't fit my needs and it's more about actual JavaScript what I don't need. Lit SSR is also not what I need because its purely JS and I wanted declarative.
Basically, it's a replacement of a custom (web) component with its template but also executing some scripts, scoping styles with CSS Modules.
I achieve this with help of set of plugins — rehype-postcss, rehype-css-modules, rehype-lodash-template (I used a simpler version of interpolation later). The plugins are stacked to process the template and finally inject as a pseudo shadow DOM into the host instead its content. The host element is converted to a fragment but is still used as a source of data (properties, extracted slot values, script function).
The host fragment can be replaced with its content after processing what is actually done but could be kept for other purposes (have no idea what, to be fair). The slot is also converted to fragment during processing to ensure the correct behaviour. Explanation in the code I linked previously.
Thanks, that helped me understand what you’re doing! It’s also a bit like what Astro is doing I believe?
I don’t quite understand the need for a fragment node type though. Or at least, why the two strategies from my previous comment (custom (temporary) node type, or a Map) are not enough?
@wooorm, thank you so much! I wish, I knew about Astro before starting own project; it seems to do something very similar to what I wanted to have but is extended further. I will reconsider future support of the project when look closer into similarities and differences.
Yes, what you recommended fits my needs. Thank you very much for the advice. I only wanted to clarify if you have any official statement about having a fragment node in the tree but I am actually satisfied with clear no answer. Please, close the issue and add a label to it if so.
Could you also clarify: for internal plugin's purpose you recommend rather defining a custom node type or using nested root is also okay?
I only wanted to clarify if you have any official statement about having a fragment node in the tree but I am actually satisfied with clear no answer
I don’t see a need to “officially” add it here. As its unclear how utilities/plugins should handle it. It’s also not something from HTML, which hast represents. And in other tools (such as the DOM or React), fragments are “unwrapped” when they are injected into another document. But you’re free to use custom nodes in trees you control.
for internal plugin's purpose you recommend rather defining a custom node type or using nested root is also okay?
I recommend a custom node. Roots as a concept represent the top-most thing. They are never a child of something. But you can store a root node at a different field. Which is what <template> does, on content: https://github.com/syntax-tree/hast#element
Good luck! :)
Hi! This was closed. Team: If this was fixed, please add phase/solved. Otherwise, please add one of the no/* labels.