javascriptsveltesvelte-5

With Svelte 5 runes mode, how can I derive properties from a dynamically rendered component


The goal:

The end goal is to simply get TabItem's title and icon to update based on the (in this example) Dashboard.svelte's title and icon from within itself, rather than the state for these properties being managed within TabItem, this makes each display component like Dashboard responsible for it's own information which makes sense to me. Here is the code I currently have:

TabsContainer.svelte

Note that this is just the snippet that really matters from here:

<div class="outer-tab-container">
    <div class="tab-items-container">
        {#each tabItems as tabItem, i}
            <TabItem
                active={activeTabIndex === i}
                component={tabItem.component}
                instance={tabItem.instance}
                onSelected={() => onTabSelected(i)}
                onClose={() => onTabClose(i)}
            />
        {/each}
    </div>

    <div class="tab-content-container">
        <!-- Render all components, but only show the active one based on index comparison -->
        {#each tabItems as tabItem, i}
            <div style="display: {activeTabIndex === i ? 'block' : 'none'};">
                <tabItem.component this={tabItem.component} bind:this={tabItem.instance} />
            </div>
        {/each}
    </div>
</div>

TabItem.svelte

This holds the component and the instance of it that was created, I think this is the bit I am miss understanding the most. Whether I put the instance in to an $effect or use $derived, instance appears null the first run through, and then it updates to a Proxy object (which is good I think...) but still it doesn't seem to have access to the title or icon properties:

<script>
    import { onMount } from "svelte";

    let {
        component,
        instance = null,
        active = false,
        onSelected = () => {},
        onClose = () => {}
    } = $props();

    let title = $state("None");
    let icon = $state("fa fa-question");

    $effect(() => {
        console.log(instance);
        if (instance) {
            title = instance.title;
            icon = instance.icon;
        }
    });
</script>

<button class="tab-item" class:active={active} onclick={onSelected} role="tab" aria-selected={active}>
    <!-- <i class={icon}></i> -->
    <span>
        {title}
    </span>
    <div class="close" onclick={onClose} title="Close tab" tabindex="0" role="button" aria-label="Close tab" aria-hidden="true" aria-controls="tab-content" aria-describedby="tab-content">
        x
    </div>
</button>

Dashboard.svelte

This is a simple example of what a tab item should be.

<script>
    import { onMount } from "svelte";

    let {
        title = "Dashboard",
        icon = "fa fa-home"
    } = $props();
</script>

<div>
    <h1>
        <i class={icon}></i>
        {title}
    </h1>
</div>

Note that I'm very aware I'm not using TS, I am just trying to understand Svelte 5's way of handling this kind of thing. Also note that I have achieved this with Svelte 4 but after trying literally 20-30 different ways of doing this I can't get it to work atall so you're seeing v31 of my attempts to get this to work so I apologise in advance if I'm barking up the completely wrong tree 😂 I've been going around in circles for 3 days now.

Any help is greatly appreciated but as I say, I would really like the Dashboard component here to be responsible for it's own information such as title and icon, I do not want the TabItem to have to maintain a seperate state for it, if that makes sense.

Thanks In advance for any help atall!😀


Solution

  • After trying a few different things including setContext and getContext, there turned out to be a fairly simple solution, here is the working code that reactively updates the TabItem title / icon whenever a tab content such as Dashboard in this example is updated:

    TabsContainer.svelte

    Can be slightly simplified (Removed setting the component on the TabItem and this={component} is not needed either):

    <div class="outer-tab-container">
        <div class="tab-items-container">
            {#each tabItems as tabItem, i}
                <TabItem
                    active={activeTabIndex === i}
                    instance={tabItem.instance}
                    onSelected={() => onTabSelected(i)}
                    onClose={() => onTabClose(i)}
                />
            {/each}
        </div>
    
        <div class="tab-content-container">
            <!-- Render all components, but only show the active one based on index -->
            {#each tabItems as tabItem, i}
                <TabContentContainer show={activeTabIndex === i}>
                    <tabItem.component bind:this={tabItem.instance} />
                </TabContentContainer>
            {/each}
        </div>
    </div>
    

    TabItem.svelte

    Here we no longer needed the component, just the instance. Also we fixed the getting of title and icon from the instance with the simple $derived rune, and then fall back to a default as instance will never be set straight away before it's mounted:

    <script>
        import { onMount } from "svelte";
    
        let {
            instance = null,
            active = false,
            onSelected = () => {},
            onClose = () => {}
        } = $props();
    
        let instanceState = $derived(instance?.state ?? {
            title: "No Title",
            icon: "fa fa-question"
        });
    </script>
    
    <button class="tab-item" class:active={active} onclick={onSelected} role="tab" aria-selected={active}>
        <i class={instanceState.icon}></i>
        <span>
            {instanceState.title}
        </span>
        <div class="close" onclick={onClose} title="Close tab" tabindex="0" role="button" aria-label="Close tab" aria-hidden="true" aria-controls="tab-content" aria-describedby="tab-content">
            x
        </div>
    </button>
    

    Dashboard.svelte

    The main issue here was me miss understanding how exporting worked with properties in Svelte 5, but it's actually incredibly simple.

    Note that the onMount here is purely to test the title updating and check that it changes within TabItem.svelte.

    <script>
        import { onMount } from "svelte";
    
        export const state = $state({
            title: "Dashboard",
            icon: "fa fa-home"
        });
    
        onMount(() => {
            setTimeout(() => { state.title = "Dashboard (Updated)"; }, 1500);
        });
    </script>
    
    <div>
        <h1>
            <i class={state.icon}></i>
            {state.title}
        </h1>
    </div>
    

    I think as I come from a C# background I am thinking of these components as objects, so I expect the instances to hold the property information themselves rather than using some sort of store.