svelte icon indicating copy to clipboard operation
svelte copied to clipboard

[Feature] Bind to text nodes with `svelte:text`

Open AlbertMarashi opened this issue 3 years ago • 3 comments

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 #text nodes, useful for page-builders, without needing to wrap in a useless element which may affect content-flow.

AlbertMarashi avatar Apr 07 '22 01:04 AlbertMarashi

This seems super niche, and solvable in userland already. Can you elaborate on why this would be useful?

Rich-Harris avatar Apr 02 '24 20:04 Rich-Harris

@Rich-Harris could we re-take a look at this? I think this feature is still important.

Please see the updated issue

AlbertMarashi avatar Sep 19 '24 10:09 AlbertMarashi

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).

Image

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)

NullVoxPopuli avatar Dec 01 '25 15:12 NullVoxPopuli