componentssveltesvelte-5

Svelte 5: Passing state and derived values from children to parent (runes mode without stores)


I have a parent component with multiple draggable Item-components which will be created dynamically in the final app. Inside each Item I calculate the current position as well as a derived offset value. Each time, any Item is dragged I want its position/offset to be shared reactively with the parent component.

In Svelte 4 with stores this was fairly easy. However, I struggle implementing this in Svelte 5 with runes. Context-API somehow isn't an option, since I'd like to define pos/offset inside the Item-component where they belong... shouldn't I?

The current code has two issues:

Update 2024-05-14

Here is a reduced verison of my code (REPL):

App.svelte

<svelte:options runes="{true}" />

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

    let CURRENT = $state({pos: {x:99, y:99}, offset: 99});

</script>

<h1>Last dragged item: ({CURRENT.pos.x} / {CURRENT.pos.y}) Offset: {CURRENT.offset}</h1>

<Item bind:CURRENT={CURRENT} itemName='ONE' />
<Item bind:CURRENT={CURRENT} itemName='TWO' />

Item.svelte

<svelte:options runes="{true}" />

<script>
    import interact from 'interactjs';

    let { CURRENT = $bindable(), itemName } = $props();
    let item;
    
    let pos = $state({x:0, y:0});
    let offset = $derived(Math.floor(Math.sqrt( Math.pow(pos.x,2) + Math.pow(pos.y,2) )));

    CURRENT.pos = pos;
    CURRENT.offset = offset;

    const handleDraggable = (node) => {
        interact(node).draggable({              
            onmove: (ev) => {
                let el = ev.target;
        
                let x = (parseFloat(el.getAttribute('data-x')) || 0) + ev.dx;
                let y = (parseFloat(el.getAttribute('data-y')) || 0) + ev.dy;
                
                el.style.webkitTransform =  el.style.transform = `translate(${x}px,${y}px)`;
                el.setAttribute('data-x', x);
                el.setAttribute('data-y', y);
                pos.x = x;
                pos.y = y;
            },
        });
    }
</script>


<div id="draggable" bind:this={item} use:handleDraggable> 
    <p>{itemName} ({pos.x} / {pos.y}) Offset: {offset}</p>
</div>

Solution

  • As hinted by @PeppeL-G in the comments to my question, variables can be passed from children to parent using callback props:

    The recommended way to pass info from parent to child is by using props, and from child to parent is by using events (in Svelte 4) and callback props (in Svelte 5). I think using that will solve your problem.

    Here is the changed code and a REPL

    App.svelte

    <svelte:options runes="{true}" />
    
    <script>
        import Item from './Item.svelte';
        
        let CURRENT = $state({pos: {x:99, y:99}, offset: 99});
        let onDragged = (pos, offset) => { 
            CURRENT.pos = pos, 
            CURRENT.offset = offset
        }
    </script>
    
    <h1>Last dragged item: ({CURRENT.pos.x} / {CURRENT.pos.y}) Offset: {CURRENT.offset}</h1>
    
    <Item itemName='ONE' {onDragged} } />
    <Item itemName='TWO' {onDragged} } />
    

    Item.svelte

    <svelte:options runes="{true}" />
    
    <script>
            import interact from 'interactjs';
    
        //let { CURRENT = $bindable(), itemName } = $props();
        let { itemName, onDragged } = $props();
        let item;
        
        let pos = $state({x:0, y:0});
        let offset = $derived(Math.floor(Math.sqrt( Math.pow(pos.x,2) + Math.pow(pos.y,2) )));
    
        const handleDraggable = (node) => {
                interact(node).draggable({              
                    onmove: (ev) => {
                        let el = ev.target;
            
                        let x = (parseFloat(el.getAttribute('data-x')) || 0) + ev.dx;
                        let y = (parseFloat(el.getAttribute('data-y')) || 0) + ev.dy;
                    
                        el.style.webkitTransform =  el.style.transform = `translate(${x}px,${y}px)`;
                        el.setAttribute('data-x', x);
                        el.setAttribute('data-y', y);
                        pos.x = x;
                        pos.y = y;
                                    onDragged(pos, offset);
                    },
                });
            }
    </script>
    
    
    <div id="draggable" bind:this={item} use:handleDraggable> 
        <p>{itemName} ({pos.x} / {pos.y}) Offset: {offset}</p>
    </div>
    
    
    <style>
        #draggable {
            background-color: green;
              width: 220px;
        }
    </style>