always_testing
always_testing

Reputation: 181

Svelte.js - How to rerender child components with new props?

I am working on creating a multi-step form using Svelte.js but I've run into an issue rendering each form page with unique props.

Here is a simple demo to show you what I mean:

// App.svelte

<script>
    import FormPage from "./FormPage.svelte";
    let formNode;

    let pageSelected = 0;

    let formPages = [
        {
            name: "email",
            label: "Email"
        },
        {
            name: "password",
            label: "Password"
        }
    ];

    const handleIncPage = () => {
        if(pageSelected + 1 < formPages.length)
        pageSelected = pageSelected + 1;        
    }

    const handleDecPage = () => {
        if(pageSelected -1 > -1)
        pageSelected = pageSelected - 1;        
    }
</script>

<form bind:this={formNode}>
    <FormPage pageData={formPages[pageSelected]} />
</form>

<button on:click={handleDecPage}>Back</button>
<button on:click={handleIncPage}>Next</button>

<p>
    Page Selected: {pageSelected}
</p>

And here's the FormPage component:

// FormPage.svelte

<script>
    export let pageData;

    const {name, label} = pageData;
</script>

<div id={`form-page-${name}`}>
    <label>Label: {label}</label>
    <input type="text" name={name} id={`input-${name}`} />
</div>

<pre>{JSON.stringify(pageData, null, 2)}</pre>

When I run the application and inc/dec pageSelected, the pageData prop changes successfully - as can be seen in the pre element. However, the label and input elements are exactly the same as they are on the first page. The id of the wrapping div element and the id of the input element are also unchanged.

When I type into the input and change the page, the text remains the same.

My goals are: to rerender the FormPage component when pageSelected changes and have the input and label change their values based on these new props. It should also be the case that when I change pages, the text already typed into the input should update and be empty (since no initial value is given to the inputs).

Having done multi-step forms in React.js, I would use a unique key attribute to make sure my FormPage rerenders every time the pageSelected state changes. But I am unsure of how to do something similar in Svelte.js.

Any suggestions?

UPDATE:

Having read this question on StackOverflow, I found out how to get the input and label elements to change as well as the id attributes. Here is my updated FormPage component:

// FormPage.svelte

<script>
    export let pageData;

    $: name = pageData.name;
    $: label = pageData.label;

</script>

<div id={`form-page-${name}`}>
    <label>Label: {label}</label>
    <input type="text" name={name} id={`input-${name}`}  />
</div>

<pre>{JSON.stringify(pageData, null, 2)}</pre>

However, the text inside the input still does not change.

Is there a way to also update the value of the input as well? It would be ideal for my use case to create an entirely new element every time the props change, but it appears that Svelte is only updating the few attributes that have changed on the same underlying DOM node.

Can Svelte be told to recreate elements in this circumstance?

UPDATE 2

So I have figured out a way to get the value of the input to change. Here is my updated FormPage component:

// FormPage.svelte

<script>
    export let pageData;

        import {onMount, afterUpdate, onDestroy} from "svelte";

        let inputNode;

        $: name = pageData.name;
        $: label = pageData.label;
        $: value = pageData.initialValue || "";

        onMount(() => {
            console.log("Mounted");
        });

        afterUpdate(() => {
            console.log("Updated");
            inputNode.value = value
        });

        onDestroy(() => {
            console.log("Destroyed");
        });

</script>

<div id={`form-page-${name}`}>
    <label>Label: {label}</label>
    <input type="text" name={name} id={`input-${name}`} bind:this={inputNode}  />
</div>

<pre>{JSON.stringify(pageData, null, 2)}</pre>

This solves the immediate problem of updating the input value.

I've added the onMount, afterUpdate and onDestroy functions to see how the component changes over time.

First, the onDestroy function is called, and then onMount after that. These both fire only once. However, afterUpdate fires every time the props change. This confirms my suspicion that the component wasn't being recreated.

Upvotes: 18

Views: 14373

Answers (2)

chmin.seo
chmin.seo

Reputation: 315

{#key}..{/key} solves the problem.

In my cases, Sizing component is used to update size(font size, or line height etc) like your FormPage.svelte

<script>
import Sizing from '..../Sizing.svelte'

const pages = {
  'font-size': Sizing,
  'line-height': Sizing, 
  ...
}
const props = {
  'font-size': { min: 8, max: 20, step: 1},
  'line-height': {min: 1, max: 3, step: 0.1}
}
let currentProp // 'font-size', 'line-height' etc ..
</script>

<div>
  {#key currentProp}
     <svelte:component this="{pages[currentProp]}" {...props}/>
  {/key}
</div>

Whenever currentProp changes, Sizing component is updated, too.

{...props} is mapped to exported variables

// Sizing.svelte
<script>
  export let min
  export let max
  export let step
</script>

Upvotes: 8

nash11
nash11

Reputation: 8690

There's a simpler way to do this. The pageData updates in your FormPage, the issue is when you are using const {name, label} = pageData;, you are basically assigning the values of the name and label parameters in pageData to two constants, name and label respectively. These two variables are no longer bound to the parent component. In order to fix this, use pageData directly in FormPage like below.

FormPage.svelte

<script>
    export let pageData;
</script>

<div id={`form-page-${pageData.name}`}>
    <label>Label: {pageData.label}</label>
    <input type="text" name={pageData.name} bind:value={pageData.value} id={`input-${pageData.name}`} />
</div>

Note: If you don't want the compiler to give you an undefined error if no pageData is passed, then you can initialize pageData as an empty object {} like so export let pageData = {};.

As for the input issue, the easiest thing to do would be to add value to formPages, and then bind the value to pageData.value as shown above in the FormPage.

New formPages data

let formPages = [{
    name: "email",
    label: "Email",
    value: ""
}, {
    name: "password",
    label: "Password",
    value: ""
}];

Here is a working example on REPL.

Upvotes: 8

Related Questions