2
votes

I'm receiving raw html snippets from a headless CMS that I need to render as-is in a Sapper application.

This also includes cases where I receive an opening tag and the corresponding closing tag as two pieces of HTML between which I add a Svelte component.

<script>
    import MyComponent from './MyComponent.svelte'

    // example snippets coming from headless CMS
    const prefix = '<p class="test">' 
    const suffix = '</p>'
</script>

<!-- use snippets as-is -->
{@html prefix}
<MyComponent />
{@html suffix}

See https://svelte.dev/repl/4c1bf80ae00e476587344b6065e7346e?version=3.19.1

However, Svelte expects each item rendered with @html to be self contained element and tries to "fix" common errors. For example, it will add a closing tag to the prefix-snippet, moving the <MyComponent/>-part out of the p.

Is there a way to circumvent this behavior in general? Or more specifically - is it possible to surround rendered components with arbitrary raw HTML?

As a side node: this runs as a sapper app and the server side rendered version of the page does correctly emit the raw HTML. It's when the client side rendering kicks in that the behavior changes.

2

2 Answers

1
votes

Here's a wicked way...

DISCLAIMER Moving Svelte managed elements under its feet can't possibly be a good idea and I totally fail to appreciate what adverse side effects you might encounter next!

I especially recommend against using this in {#each} blocks, and especially keyed ones, since Svelte will want to reorder its elements and might be upset if they're not where it expects them in the DOM.

... but maybe it can get you unstuck for simple cases. You be the judge.

My idea is to render the full concatenated string (prefix + suffix) with an additional element I can grab and replace with a component already rendered independently by Svelte, outside of this fragment.

Here's the trick:

{@html prefix + '<span id="vslot"></span>' + suffix}

Here's an example component that implements the trick:

<script>
    import { afterUpdate } from 'svelte'

    export let prefix
    export let suffix

    let wrap
    let content

    afterUpdate(() => {
        const vslot = wrap.querySelector('#vslot')
        vslot.parentNode.replaceChild(content, vslot)
    })
</script>

<div bind:this={wrap}>
    {@html prefix + '<span id="vslot"></span>' + suffix}
</div>

<div bind:this={content}>
    <slot />
</div>

You would consume it like this:

<Wrapper {prefix} {suffix}>
    <MyComponent />
</Wrapper>

REPL

0
votes

You can use slots in order to achieve a similar result by wrapping component inside another component. Here is official documentation: https://svelte.dev/tutorial/slots