svelte icon indicating copy to clipboard operation
svelte copied to clipboard

Svelte 4 and 5 adds spaces between html elements when mounting components through JavaScript

Open abtinturing opened this issue 1 year ago • 10 comments

Describe the bug

I was initially working in Svelte 4, trying to mount components via JS like so:

new MyComponent({
    target: container,
    hydrate: false
});

But when I started looking at container's innerHTML after mounting the component, I would see spaces being added in between the elements of the component, even though there shouldn't have been any (or I at least think so). For example, I would have the following component:

<script>
	const data = { content: 'hello', lang: 'js' };
</script>

<div>{data.lang}</div>
<div>{@html data.content}</div>

and the innerHTML would be this:

"<div>js</div> <div>hello</div>"

As you can see, there is a whitespace just before the last div. My first attempt at trying was to remove the line break between the two divs, and that fixed it:

<div>{data.lang}</div><div>{@html data.content}</div>

Output innerHTML (good):

"<div>js</div><div>hello</div>"

But whenever I press save on my editor, I believe prettier reverts it back and puts the 2 divs on separate lines, which is expected. And given the complexity of my original component, not only would I have to somehow make sure to never format my components, but also that my components end up having like 20 elements jammed into one line like so: Screenshot 2024-08-07 212947 Which is unreadable.

I read that Svelte 5 had fixed some issues with whitespaces, but after trying it out using the new mount API, not only does my problem remain, but it's gotten even worse. Now there is random comments everywhere, in odd locations, though this is less of a breaking issue than the spaces. For the example above, this is what I get in Svelte 5:

"<div>js</div> <div>hello<!----></div>"

Here's a more complicated example in Svelte 5, with comments everywhere:

"<!----><div class="code-block" spellcheck="false"><div class="line"><span class="symbol">```</span><span class="lang">js</span><!----></div> <!----><div class="line">hello<!----></div><!----> <div class="line"><span class="symbol">```</span></div><!----></div>"

And as you can see, with there are the same extra spaces on both.

This is a breaking issue in context of my application, as it creates text nodes that add random spaces to the element, impacting innerText, as well as messing up my text node iterator by feeding it spaces that I didn't intend to be there.

Reproduction

I have a public repo with the most simple reproduction of this here.

Here is what my +page.svelte looks like: Svelte 4:

<script>
	import MyComponent from '$lib/MyComponent.svelte';
	import { onMount } from 'svelte';

	onMount(() => {
		const container = document.createElement('div');
		new MyComponent({ target: container, hydrate: false });

		console.warn({ innerText: container.innerText, container, innerHTML: container.innerHTML });
	});
</script>

Svelte 5:

<script>
	import MyComponent from '$lib/MyComponent.svelte';
	import { mount, onMount } from 'svelte';

	onMount(() => {
		const container = document.createElement('div');
		mount(MyComponent, { target: container });

		console.warn({ innerText: container.innerText, container, innerHTML: container.innerHTML });
	});
</script>

and my simplified component looks like this:

<script>
	const data = { content: 'hello', lang: 'js' };
</script>

<div>{data.lang}</div>
<div>{@html data.content}</div>

Logs

{
    "innerText": "js hello",
    "container": {},
    "innerHTML": "<div>js</div> <div>hello<!----></div>"
}

System Info

System:
    OS: Windows 11 10.0.22635
    CPU: (16) x64 AMD Ryzen 7 2700X Eight-Core Processor
    Memory: 44.14 GB / 63.95 GB
  Binaries:
    Node: 18.18.0 - C:\Program Files\nodejs\node.EXE
    Yarn: 1.22.21 - ~\AppData\Roaming\npm\yarn.CMD
    npm: 9.8.1 - C:\Program Files\nodejs\npm.CMD
    pnpm: 9.2.0 - C:\Program Files\nodejs\pnpm.CMD
  Browsers:
    Edge: Chromium (127.0.2651.8)
    Internet Explorer: 11.0.22621.3566
  npmPackages:
    svelte: ^5.0.0-next.1 => 5.0.0-next.210

Severity

blocking an upgrade

abtinturing avatar Aug 08 '24 02:08 abtinturing

A very simple solution that should also make prettier happy is to move the initial part of the tag on the first line

<script>
	const data = { content: 'hello', lang: 'js' };
</script>

<div>{data.lang}</div><div
    >{@html data.content}</div>

As you can see in the linked repl it works, the comments are needed for hydration so I don't think you can do much about it. What's your use case in general?

paoloricciuti avatar Aug 08 '24 05:08 paoloricciuti

Although I've had some luck with that trick in another component, I couldn't make prettier happy in the example that you provided. But I did figure out the following works:

<script>
	const data = { content: 'hello', lang: 'js' };
</script>

<div>{data.lang}</div><!--
--><div>{@html data.content}</div>

But that fix doesn't work on a this other case where it involves if and each blocks. I've tried jamming everything into one line and at one point (I've left a comment inside the code), prettier keeps putting separating the </div> and {#if data.co... into their own line. The output innerText that I'm getting is this:

"```js hello```"

When it really should be this:

"```jshello```"

Here's the component code that reproduces that:

<script>
	const data = { content: 'hello', lang: 'js', start: '```', end: '```' };
</script>

{#if data}
	<div class="code-block" spellcheck="false">
		<div class="line">
			<span class="symbol">{data.start}</span>{#if data.lang}<span class="lang">{data.lang}</span
				>{/if}
		</div> <!-- This part right here -->
		{#if data.content}{#each data.content.split('\n') as line}<div class="line">
					{@html line}
				</div>{/each}{/if}{#if data.end}<div class="line">
				<span class="symbol">{data.end}</span>
			</div>
		{/if}
	</div>
{/if}

As you can also see, the code at this point is becoming unreadable, which I don't think should be normal.

I have updated the repo, to now include this new case, as well as the previous. It runs 2 simple tests checking to see if we are getting the expected result.

abtinturing avatar Aug 08 '24 06:08 abtinturing

Again, not trying to be rude or anything but...are you sure a component like this is the best tool for your use case? What are you trying to achieve?

paoloricciuti avatar Aug 08 '24 07:08 paoloricciuti

Yes I believe it is the best tool for my use case, if I can avoid the random whitespaces, which shouldn't be there to begin with. These components are to be placed dynamically via Javascript based on where I find matches via regex. They are to have unique functionalities, such as interactivity, animations, and more, which is what I use svelte for. The very first time I process a piece of text, it works correctly, but if I were to run regex again, I would need to do it on the collected innerText of all the elements, and if the components add random spaces in that aren't meant to be there, then our regex will either not work or provide incorrect information. Their translation back to innerText is very important so that my regular expressions work correctly, hence why this is a major problem.

abtinturing avatar Aug 08 '24 07:08 abtinturing

shouldn't be there to begin with

Then you have screwed up

<i>Hello</i> <b>World</b>

and you should insert ugly {' '} between elements like in JSX.

There is no universal solution, and the problem of whitespaces in HTML is as ancient as the Internet. The current behaviour is desired or doesn't matter in most use cases. But removing whitespaces is cumbersome and even more ugly than {' '}.

It would be nice if there were an option to stip out whitespaces completely.

7nik avatar Aug 08 '24 10:08 7nik

Indeed our prettier plugin pretends that white space at the top level doesn't matter (i.e. as if the component is a block element). It's the desired behavior in 99% of all cases but not all as seen here. So yes maybe we need another whitespace option to strip all whitespace?

dummdidumm avatar Aug 08 '24 11:08 dummdidumm

Indeed our prettier plugin pretends that white space at the top level doesn't matter (i.e. as if the component is a block element). It's the desired behavior in 99% of all cases but not all as seen here. So yes maybe we need another whitespace option to strip all whitespace?

I think it could be a good idea but should it strip literally every whitespace? I also think an option to SSR without hydration markers would be good too

paoloricciuti avatar Aug 08 '24 11:08 paoloricciuti

should it strip literally every whitespace?

If you refer to <pre> and other exceptional cases, I think the user can move it out to another component where whitespaces are preserved. After all, the goal of the option is total control over the whitespace insertions.

also think an option to SSR without hydration markers would be good too

Sounds good for when CSR is off.

7nik avatar Aug 08 '24 11:08 7nik

Sounds good for when CSR is off.

That or when you want to render to show the actual code rather than render the output

paoloricciuti avatar Aug 08 '24 12:08 paoloricciuti

Indeed our prettier plugin pretends that white space at the top level doesn't matter (i.e. as if the component is a block element). It's the desired behavior in 99% of all cases but not all as seen here. So yes maybe we need another whitespace option to strip all whitespace?

I don't think that would solve the OP's issue here fully as they also don't want our hydration markers. I don't think there is a silver bullet here and what we have right now is fine. This use-case is not a common case and one I'd argue can be worked around with in user-land.

trueadm avatar Aug 10 '24 19:08 trueadm