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.
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.
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.
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.
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.
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 nopageData
is passed, then you can initializepageData
as an empty object{}
like soexport 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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With