Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 576
always_testing Avatar asked Sep 19 '19 12:09

always_testing


People also ask

Does child component Rerender when props change?

This doesn't only mean the component's render function will be called, but also that all its subsequent child components will re-render, regardless of whether their props have changed or not.

Does a component Rerender if props change?

In order for props to change, they need to be updated by the parent component. This means the parent would have to re-render, which will trigger re-render of the child component regardless of its props.

How do I render a component on props change?

React components automatically re-render whenever there is a change in their state or props. A simple update of the state, from anywhere in the code, causes all the User Interface (UI) elements to be re-rendered automatically. However, there may be cases where the render() method depends on some other data.

How do you pass component as prop in Svelte?

The solution. To pass Svelte components dynamically down to a child component you have to use Svelte's <svelte:component> directive. <svelte:component> accepts a property called this . If the property this has a component attach to it than it will render the given Svelte component.


1 Answers

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.

like image 150
nash11 Avatar answered Oct 24 '22 00:10

nash11