Search code examples
javascripthtmljavascript-objectssveltesveltekit

Passing objects as props in Svelte makes the object fields not change in the HTML?


I have this Svelte code:

Player.svelte:

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

    let audio;
</script>

<audio src={...} bind:this={audio} />
<PlayerControls {audio} />

PlayerControls.svelte:

<script>
    import PlayIcon from '../icons/PlayIcon.svelte';
    import PauseIcon from '../icons/PauseIcon.svelte';

    export let audio;

    const onClick = () => {
        if (audio?.paused) audio.play();
        else audio.pause();
    };
</script>

{#if audio?.paused}
    <PlayIcon {onClick} />
{:else}
    <PauseIcon {onClick} />
{/if}

If I press the play icon, the icon doesn't change, but the audio starts, and if I click again, the audio stops, and the icon is unchanged. Seems like audio.paused "changes" only in the script, but not in the html. What is wrong here, and what I'm not understanding here about Svelte?


Solution

  • The most robust solution in your given situation is to leverage the specific <audio> element events to alert svelte to reinspect the state of the ref. This allows svelte to manage your listener lifecycle, and also allows the element itself to handle all the edge cases of when/how the state of playback changed.

    <script>
      import { onMount, onDestroy } from 'svelte';
      import PlayerControls from './PlayerControls.svelte';
    
      let audio;
    
      const audioUpdated = (e) => {
        audio = audio;
      };
    </script>
    
    <audio
      bind:this={audio}
      src="https://interactive-examples.mdn.mozilla.net/media/cc0-audio/t-rex-roar.mp3"
      on:pause={audioUpdated}
      on:play={audioUpdated}
      on:ended={audioUpdated}
    />
    
    <PlayerControls {audio} />
    

    REPL

    Original Answer

    A straightforward solution to your problem is covered in the link to the documentation in my comment Reactivity / Updating arrays and objects and is to simply reassign audio as the last step in your onClick handler.

    Keep in mind that this doesn't track the change in paused if the audio finishes on its own.

    const onClick = () => {
        if (audio?.paused) audio.play();
        else audio.pause();
        
        audio = audio;
    };
    

    REPL

    Edit

    I initially recommended that one could mutate the paused property directly:

    const onClick = () => {
      if (audio?.paused) {
        audio.play();
        audio.paused = false;
      } else {
        audio.pause();
        audio.paused = true;
      }
    };
    

    But neglected the fact that audio is a ref and paused is a readonly property.