[Feature] Bind to text nodes with `svelte:text`
Describe the problem
Having the ability to bind to text or comment nodes would be valuable for people developing CMS tools, page builders and other content-editing related projects.
There's really only two options we have as developers here:
Option 1
Inserting empty dom elements to be able to get the next sibling and get it's .nextSibling
[!Caution] introduces junk elements into the dom that serve no purpose except to get the next sibling
<script>
let text = "hello"
let ref
let text_node
onMount(() => {
text_node = ref.nextSibling;
ref.remove() // clean up the junk element
})
</script>
<span bind:this={ref} style="display:none"></span>
{ text }
Option 2
Wrapping the text nodes in things like <span style:display="contents"> and getting the .firstChild
[!Caution] changes the semantics of the content, and leads to unnecessary "divitis" within content editing tools such as CMSs or rich text editors.
<script>
let text = "hello"
let ref
let text_node
onMount(() => {
text_node = ref.firstChild;
})
</script>
<span bind:this={ref} style="display:contents">{ text }</span>
So as it stands, it's currently not possible in userland to gain reference to a text node without having to create junk elements.
Additionally, this would allow for the use of things like use:action on the <svelte:node> tags.
The use-cases here, while somewhat niche and related to CMS tools, contenteditable functionality are very valuable - we often need to attach metadata to DOM nodes, or access properties such as .textContent .isSameNode(), .insertBefore() .cloneNode(), .previousSibling .parentElement, .parentNode ideally without needlessly introducing junk elements into the DOM tree
Proposed solution
<script>
let text = "hello"
let text_node
</script>
<svelte:text bind:this={text_node} value={text}/> <!-- no junk elements created -->
Related issues:
https://github.com/sveltejs/svelte/issues/4544
Describe the proposed solution
Ability to bind to a special svelte element type such as <svelte:text> or <svelte:comment> or the proposed <svelte:element tag="#text">
Text Nodes are a valid Node in HTML, and come with all the properties and methods that people will find valuable to be able to bind to, such as .after() .nextSibling .parentNode and so on.
Editors like Prose mirror attach metadata to nodes in order to count cursor offsets and lengths, and being able to do this would be valuable.
Alternatives considered
Creating a useless element and binding to that in order to get to the text node, eg:
<script>
let ref;
onMount(() => {
const child = ref.nextElementSibling;
});
</script>
<span bind:this={ref} style="display:none"></span>
<slot>
Importance
- [x] Allows binding to
#textnodes, useful for page-builders, without needing to wrap in a useless element which may affect content-flow.
This seems super niche, and solvable in userland already. Can you elaborate on why this would be useful?
@Rich-Harris could we re-take a look at this? I think this feature is still important.
Please see the updated issue
I have a usecase that requires synchronous access to a text node -- and this approach works in ember, fwiw.
I have a utility for dynamically calculating the correct heading level based on the DOM structure (without use of Context or any other framework-hierarchy).
Playground for reference: https://svelte.dev/playground/45e9e53c9b5b4d0bbaaf7e71acde7893?version=5.45.2 Working ember implementation: https://ember-primitives.pages.dev/3-ui/heading.md Shared library code: https://ember-primitives.pages.dev/6-utils/get-section-heading-level.md
There are only 2 requirements:
- be able to have access to text node synchronously (for optimized rendering, ofc)
- lazily calculate the heading level (this part is easy)
So, in Svelte, I'd like this to work:
<script>
import { getSectionHeadingLevel } from "which-heading-do-i-need";
const { children } = $props();
const node = document.createTextNode('hi');
const hLevel = $derived.by(() => {
return `h${getSectionHeadingLevel(node)}`;
});
</script>
{node}
<svelte:element this={hLevel}>
{@render children()}
</svelte:element>
but, the node is currently rendered as [object Text]
rendering Text Nodes and Element Nodes directly is a low level feature, sure, but it's super worth it for doing stuff like this (or dom-context, resize-observers, or any other thing where it's useful to have a direct reference to a DOM Node before render occurs)