Search code examples
htmlcheckboxtreeviewsvelte

How to create dynamic tree structure in svelte?


I need a treeview in svelte that has checkboxes with each node.

I am unable to get functionality that if we check a parent node then all of its (only its) children whether they are file of folders, all of them (child) get checked automatically and any of them is unchecked later then the parent should also get unchecked. How can I achieve this?

Below is the code that I followed from here. I tried few things but no luck.

App.svelte-

<script>
    import Folder from './Folder.svelte';

    let root = [
        {
            name: 'Important work stuff',
            files: [
                { name: 'quarterly-results.xlsx' }
            ]
        },
        {
            name: 'Animal GIFs',
            files: [
                {
                    name: 'Dogs',
                    files: [
                        { name: 'treadmill.gif' },
                        { name: 'rope-jumping.gif' }
                    ]
                },
                {
                    name: 'Goats',
                    files: [
                        { name: 'parkour.gif' },
                        { name: 'rampage.gif' }
                    ]
                },
                { name: 'cat-roomba.gif' },
                { name: 'duck-shuffle.gif' },
                { name: 'monkey-on-a-pig.gif' }
            ]
        },
        { name: 'TODO.md' }
    ];
</script>

<Folder name="Home" files={root} expanded/>

File.svelte-

<script>
    export let name;
    $: type = name.slice(name.lastIndexOf('.') + 1);
</script>

<span style="background-image: url(tutorial/icons/{type}.svg)">{name}</span>

<style>
    span {
        padding: 0 0 0 1.5em;
        background: 0 0.1em no-repeat;
        background-size: 1em 1em;
    }
</style>

Folder.svelte-

<script>
    import File from './File.svelte';

    export let expanded = false;
    export let name;
    export let files;
    let checkedState = true;

    function toggle() {
        expanded = !expanded;
    }
    
    function onCheckboxChanged(e){
        console.log("out: "+document.getElementById("cb1").checked)
    }
</script>

<input type="checkbox" on:change={onCheckboxChanged} id="cb1" bind:checked={checkedState}><span class:expanded on:click={toggle}>{name}</span>

{#if expanded}
    <ul>
        {#each files as file}
            <li>
                {#if file.files}
                    <svelte:self {...file}/>
                {:else}
                    <input type="checkbox" on:change={onCheckboxChanged} id="cb1" bind:checked={checkedState}><File {...file}/>
                {/if}
            </li>
        {/each}
    </ul>
{/if}

<style>
    span {
        padding: 0 0 0 1.5em;
        background: url(tutorial/icons/folder.svg) 0 0.1em no-repeat;
        background-size: 1em 1em;
        font-weight: bold;
        cursor: pointer;
    }

    .expanded {
        background-image: url(tutorial/icons/folder-open.svg);
    }

    ul {
        padding: 0.2em 0 0 0.5em;
        margin: 0 0 0 0.5em;
        list-style: none;
        border-left: 1px solid #eee;
    }

    li {
        padding: 0.2em 0;
    }
</style>


Solution

    1. Remove binding in checkbox next to File component. This will prevent from case when you click File's checkbox and whole folder get checked. Instead you need just to pass state to child File components when Folder gets checked. So change code like this:
    <!-- <input type="checkbox" bind:checked={checkedState}> -->
    <input type="checkbox" checked={checkedState}>
    
    1. Then you need to pass Folder's checked state to all child Folder components (so when you check folder, folders inside would be checked too). You can do it by making checkedState a prop, not just regular variable, then just pass this prop when calling svelte:self component
    <script>
        // let checkedState = true;
        export let checkedState = true;
    </script>
    
    <!-- <svelte:self {...file}/> -->
    <svelte:self {...file} {checkedState} />
    
    1. After that only one problem case left. When all child components are (un)checked parent should be (un)checked too. I can't really see possibility to implement this thing with current structure. I would recommend you to move checkbox inside File component and then create field checked or selected in file object (in App.svelte) so you could easily manage all states in any child component. It also would be easier to read this values when extracting data from root Folder component (e.g. if you will need to send this data to server). After adding prop you of course should work with object properties, not component's state. Good luck with it!

    P.S: Don't use static ids in your components and even more so in each loops. This will make your output html invalid.