Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

[mount] Svelte 5, create .svelte component **with content** from JS (programatically) #15105

Answered by brunnerh
Neah-Ko asked this question in Q&A
Discussion options

Hello,

Recent svelte adopter (and so far mostly enjoyer), I'm trying to find a way to create svelte components from javascript code.

Note that I'm using it in SPA mode, so I do not have access to render()

The feature I'm trying to achieve, is on some user action (in this case a large file upload) create a card displaying progress

Here a very simple case using pure html:

<script lang="ts">
 function btnClick(){
 let card = document.createElement('div');
 let cardcontent = document.createElement('p');
 cardcontent.innerText = "Pending Action ...";
 card.appendChild(cardcontent); 
 let cardbar = document.getElementById("mycards")!;
 cardbar.appendChild(card); 
 }
</script>
<button type"button" onclick={btnClick()}>
click me
</button>
<div id="mycards"></div>

In my case I'm using flowbite-svelte, so the idea would be to create a svelte component. Which doesn't work with document.createElement

Browsing the docs, using mount seem to be the most up to date syntax

import { Card } from 'flowbite-svelte';
function btnClick(){
 let cardbar = document.getElementById("mycards")!;
 let new_card = mount(Card, {target: cardbar});
}

It works for the card, but now it's not obvious to me how to set the content, I've tried a few things

Idea 1:

I thought about using mount all the way down. however it only accepts svelte components as input so elements created with document.createElement wont work here

Idea 2:

Given my understanding on how svelte works. The content is actually a snippet labelled children from the component point of view, so If I create a snippet from js and pass it as children prop I should be able to set the content of that component. Moreover there exist a createRawSnippet function

function btnClick(){
 let cardbar = document.getElementById("mycards")!;
 const content = `<p>Pending Action ...</p>`
 const cardcontent = createRawSnippet(() => {
 return {
 render: () => content,
 setup: (node) => {
 $effect(() => {
 node.textContent = content;
 });
 }
 }}
 );
 let new_card = mount(Card, {
 target: cardbar,
 props: {
 class: "action-card",
 children: cardcontent,
 },
 })
}

Which appears to be correct for my JS linters, but does not display the content inside the card. I'm not sure this setup argument should be set for static content but I've also tried for good measure.

Idea 3 (aka 0'):

That card is just a div with a few tailwind classes behind the hood so I'm going to go with a pure JS approach for time being however next time I'm encountering the use case I would like to know if there exist a way to achieve this with svelte primitives.

Best,

You must be logged in to vote

It depends on what the content is and where it is supposed to come from. You can e.g. reference snippets defined in the markup and snippets that don't use local state can also be exported from the module script. That may be easier than working with createRawSnippet which is not really meant for general use.

One can also import components and pass those as props & add them in the receiving component's markup.

Regarding the approach using createRawSnippet, that works if the Card actually renders children.
If that is not the case, the library may still be using <slot />.
You could create a wrapper component as a workaround (so you have <Card {...rest}>{@render children()}</Card>).

Though if ...

Replies: 2 comments 4 replies

Comment options

It depends on what the content is and where it is supposed to come from. You can e.g. reference snippets defined in the markup and snippets that don't use local state can also be exported from the module script. That may be easier than working with createRawSnippet which is not really meant for general use.

One can also import components and pass those as props & add them in the receiving component's markup.

Regarding the approach using createRawSnippet, that works if the Card actually renders children.
If that is not the case, the library may still be using <slot />.
You could create a wrapper component as a workaround (so you have <Card {...rest}>{@render children()}</Card>).

Though if you create a wrapper, you could also add an element inside the card and expose that; this way you can add DOM elements directly to the component.
Example:

<!-- CardWrapper.svelte -->
<script>
	import Card from './Card.svelte';

	const { ...rest } = $props();

	let contents;

	export { contents }
</script>
<Card {...rest}>
	<div bind:this={contents}></div>
</Card>
<script>
	import { mount } from 'svelte';
	import CardWrapper from './CardWrapper.svelte';

	let cards;

	async function btnClick() {
		const content = document.createElement('p');
		content.textContent = 'Pending Action...';

		const card = await mount(CardWrapper, {
			target: cards,
			props: { class: "action-card" },
		});
		card.contents.append(content);
	}
</script>
<button onclick={btnClick}>Add</button>
<div bind:this={cards} ></div>
You must be logged in to vote
4 replies
Comment options

Oh I see, that's a bit convoluted but certainly not as bad as working with createRawSnippet indeed.

Thanks a lot for the pattern, I'll be using it !

Comment options

So I'm actually testing this today and while this works in the playground, I'm getting errors when putting it in practice in my local dev environment.

While using svelte 5.1.16, this was causing errors when using mount.

I've upgraded to the latest version, but now getting that contents is undefined.

The first pattern where CardWrapper is rendering children in conjunction with createRawSnippet did worked, but allow for less flexible editing.

Comment options

Did you actually await the mount?

Comment options

Oh, no that must've been my mistake. I did not see that mount is async.

This still gave me inspiration, and instead of going for a generic Card wrapper I went for a specialized one, with fixed content and exposing the progress of file upload instead which works neatly:

<!-- UploadCard.svelte -->
<script>
 import { Card, Progressbar } from 'flowbite-svelte';

 let {progress, filename, ...rest } = $props();
 export {progress};
</script>
<Card {...rest}>
 <h5>Upload -- {filename}</h5>
 <Progressbar bind:progress/>
</Card>

Then later:

let up_card = mount(UploadCard, {
 target: action_bar!,
 props: {
 class: "action-card w-fit",
 filename: ...,
 progress: "0"
 }
 }
// up_card.progress can be edited from "0" to "100" during large file upload
...
Thanks for your help !
Answer selected by Neah-Ko
Comment options

I think this could be accomplished much more idiomatically with an array.

Basic example

The most basic example is simply iterating over the array and showing static content.

<script>
 let uploads = $state([])
 async function handleClick() {
 const id = Date.now()
 const filename = id + '.txt'
 
 let upload = { id, filename }
 
 uploads.push(upload)
 }
</script>
<button onclick={handleClick}>Add</button>
{#each uploads as upload (upload.id)}
 <div>
 <p>Pending action...</p>
 <p>{upload.filename}</p>
 </div>
{/each}

With mutations

Once the value is captured in state, it can be updated directly. You can update, add, or remove from the array to control what components are rendered.

<script>
 let uploads = $state([])
 async function handleClick() {
 const id = Date.now()
 const filename = id + '.txt'
 
 let upload = { id, filename, progress: 0 }
 
 uploads.push(upload)
 // Once the value is added to the state array, it gets proxified.
 // Afterwards, mutating the proxified value will cause updates.
 // You can retreive the proxified version of the inserted value by doing something like this.
 upload = uploads.find(u => u.id === id) || uploads.at(-1)
 // This should cause an update for example.
 upload.progress += 10
 // Example of updating state.
 const interval = setInterval(() => {
 // You can also look up the upload by ID every time for the most safety.
 // This is equivalent with using the proxified state retrieved right before if it is still present in the array.
 const current = uploads.find(u => u.id === id)
 
 if (current) {
 current.progress += 10
 
 if (current.progress >= 100) {
 clearInterval(interval)
 // You could also remove the upload altogether.
 uploads = uploads.filter(u => u.id !== id)
 }
 }
 }, 1_000)
 // If you want to be extra careful for this example, clean up the intervals.
 upload.interval = interval
 }
 
 $effect(() => {
 return () => {
 // If you want to be extra careful for this example, clean up the intervals.
 uploads.forEach((upload) => {
 clearInterval(upload.interval)
 })
 }
 })
</script>
<button onclick={handleClick}>Add</button>
{#each uploads as upload (upload.id)}
 <div>
 <p>Pending action...</p>
 <p>{upload.filename}</p>
 <p>{upload.progress}</p>
 </div>
{/each}

With custom components

If you also want dynamic components for each upload, you could capture that in the array.

<script>
 import { Card } from 'flowbite-svelte'
 import MyComponent from './my-component.svelte'
 let uploads = $state([])
 // Dummy value.
 let i = 0;
 async function handleClick() {
 const id = Date.now()
 const filename = id + '.txt'
 
 let upload = { id, filename, progress: 0 }
 // Just an example, not representative of actual practices.
 i++
 if (i % 3 === 1) {
 upload.component = Card
 }
 if (i % 3 === 2) {
 upload.component = MyComponent
 }
 uploads.push(upload)
 }
</script>
<button onclick={handleClick}>Add</button>
{#each uploads as upload (upload.id)}
 {#if upload.component}
 <upload.component>
 <p>Pending action...</p>
 <p>{upload.filename}</p>
 <p>{upload.progress}</p>
 </upload.component>
 {:else}
 <div>
 <p>Pending action...</p>
 <p>{upload.filename}</p>
 <p>{upload.progress}</p>
 </div>
 {/if}
{/each}
You must be logged in to vote
0 replies
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Category
Q&A
Labels
None yet

AltStyle によって変換されたページ (->オリジナル) /