27

I have a component called WastedTime.svelte with a value wastedTime. There's also a function to change the value to 50 (in my real code, this does an animation but this is a reduced test case for Stack Overflow).

To allow the child function to be called from a parent, I have used <script context="module"> per the Svelte docs:

<script context="module">
    var wastedTime = 0;
    export function changeNumber(){
        console.log('changing number')
        wastedTime = 50
    }
</script>

<script>
    // Putting 'var wastedTime = 0' here doesn't work either
</script>


<h1>Wasted time: {wastedTime}</h1>

The parent calls the function in the child from onMount:

<script>

    import { onMount } from 'svelte';
    import WastedTime, {changeNumber } from './WastedTime.svelte';

    onMount(() => {
        changeNumber()
    });
</script>

<WastedTime />

The problem is that since wastedTime is referred to in <script context="module">, it can't seem to change wastedTime. The exported function runs, but wastedTime stays at 0.

Copy of this on Svelte REPL

I have tried: - Putting var wastedTime = 0 in <script context="module"> - Putting var wastedTime = 0 in <script>

Neither works.

How can I export a function from a Svelte component that changes a value in the component?

mikemaccana
  • 110,530
  • 99
  • 389
  • 494

4 Answers4

41

<script context="module"> isn't reactive — changes to variables inside this block won't affect individual instances (unless you were changing a store value, and every instance was subscribed to that store).

Instead, export the changeNumber function directly from the instance, and get a reference to it with bind:this:

WastedTime.svelte

<script>
    var someNumber = 0;
    export function changeNumber(){
        console.log('changing number')
        someNumber = 56
    }
</script>

<h1>Wasted time: {someNumber}</h1>

App.svelte

<script>
    import { onMount } from 'svelte';
    import WastedTime from './WastedTime.svelte';

    let wastedTimeComponent;

    onMount(() => {
        wastedTimeComponent.changeNumber()
    });
</script>

<WastedTime bind:this={wastedTimeComponent} />

Demo here: https://svelte.dev/repl/f5304fef5c6e43edb8bf0d25d634f965

Rich Harris
  • 28,091
  • 3
  • 84
  • 99
  • Thanks. I swear I read an example in the documentation that said ` – mikemaccana Oct 09 '19 at 13:47
  • you're probably thinking of https://svelte.dev/tutorial/module-exports – Rich Harris Oct 09 '19 at 14:25
  • 1
    t would be great to explain the difference between 1) "Anything exported from a context="module" script block becomes an export from the module itself. " and 2) "changes to variables inside this block won't affect individual instances". Should I export variables from a `script` with `context="module"` or not? – mikemaccana Oct 25 '19 at 15:25
  • 6
    This question and answer need to go into the Svelte documentation! Without this pattern being explained, it's a three-ring-circus to communicate to a component. – bob Mar 24 '20 at 23:44
  • Another way is to separate functions in a WastedTimeService.js with `exported` functions or as a separate js-class. This ongoing makes it more easy to unit-test the functions. – nologin Jul 11 '20 at 08:01
  • @Rich Actually, the REPL example does not work, on click the following error is produced: `Error: message: "Cannot read property 'call' of undefined" stack: TypeError: Cannot read property 'call' of undefined at HTMLButtonElement.eval (eval at handle_message (about:srcdoc:13:8), :59:23)` – Tomasz Plonka Sep 10 '20 at 07:13
  • there seems to be some problem with the version of Svelte referenced by that link. not sure what it was, but the updated link (using the current version) works fine for me – Rich Harris Sep 10 '20 at 14:03
11

Simplified version of Rich's answer:

App.svelte

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

let timeComponent;
let time;
</script>

<Time bind:this={timeComponent} bind:time />

<h1>Spent time: {time} ms</h1>

{#if timeComponent}
  <button on:click={() => timeComponent.spendTime() }>Click me</button>
{:else}
  Loading...
{/if}

Time.svelte

<script>
export var time = 0;
export function spendTime() {
  time += 50;
}
</script>

The key here is export function.

Ben Bucksch
  • 395
  • 3
  • 13
4

Well, that's a damn good question!

My First Attempt:

I've tried @Rich Harris answer (export a function from within the component, bind it when using the component with onMount event listener). The concept is indeed solid, but my problem was a little bit more complex - I've tried to pass an event with parameter to be invoked from the outside. And yes - this parameter is forged using {#await}.. so... no luck with that approach


My Second Attempt:

After learning more about how svelte handles events (I'm a newbie to svelte and learning as I progress), I've found this great article. In short: let's make our component truly-event oriented! In your question - you actually trying to implement onload event... so.. why shouldn't we do just that?

component.svelte

<script>
  import { createEventDispatcher, onMount } from 'svelte';
  const dispatch = createEventDispatcher();

  function doSomething() { console.log('wowza!') }
  
  // wait for onMount to trigger, then dispatch event named "load"
  onMount(() => dispatch('load', 
  { 
    data: 'yay',
    doSomething
  }));
</script>

app.svelte

<script>
  import { Component } from './component.svelte';

  function component_load(event)
  {
    console.log(event.detail.data);
    event.detail.doSomething();
  }
</script>
<Component on:load={component_load} />

So - yep, I've learned something today! Also:

  • This is a more elegant way to code (event-driven)
  • Component expose itself with proper usage of Svelte events life-cycle
  • dispatch can be triggered in response to other events - letting you constuct a complete life-cycle for your component

AMAZING!

ymz
  • 6,602
  • 1
  • 20
  • 39
1

A Simpler Solution

Parent component:

<script>
    import Counter from './Counter.svelte'
    let counterElement
</script>

<Counter bind:this={counterElement} />

<button on:click={_=> counterElement.add()}>Add</button>

Child component:

<script>
    export const add =_=> count += 1
    let count = 0
</script>

<div>Count: {count}</div>

Check it out:

https://svelte.dev/repl/70cff59aee444dd3ab772a244bd8fa36?version=3.48.0

babakfp
  • 164
  • 6