-
-
Notifications
You must be signed in to change notification settings - Fork 4.6k
-
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,
Beta Was this translation helpful? Give feedback.
All reactions
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
-
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>
Beta Was this translation helpful? Give feedback.
All reactions
-
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 !
Beta Was this translation helpful? Give feedback.
All reactions
-
👍 1
-
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.
Beta Was this translation helpful? Give feedback.
All reactions
-
Did you actually await
the mount
?
Beta Was this translation helpful? Give feedback.
All reactions
-
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 !
Beta Was this translation helpful? Give feedback.
All reactions
-
👍 1
-
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}
Beta Was this translation helpful? Give feedback.