im

Calculator fields in Svelte forms

There are situations when one form field has to act as input to another

Sometimes, when working with forms in Svelte, there are situations where you need one input field to drive the value of another field. An example would be a blog editor where title form field generates the slug for the blog post. It acts as a driver for it.

Another typical example would be some type of interest calculator form. You change one field and that change forces other fields to recalculate their values.

The first thing that comes to mind is to use Svelte's bind directive, and you are right, but that alone is not enough. We have to use unidirectional (one-way) bindings, made popular by React.

Bidirectional Bindings#

By default, Svelte offers bidirectional (two-way) bindings. You bind the input's values with bind:value for text type inputs and bind:checked for checkboxes.

<script>
const values = {
framework: 'Svelte',
userAgrees: false
}
</script>

<div>
<input
type="text"
name="framework"
id="framework"
bind:value=
{values.framework}
/>

{values.framework}
</div>

<div>
<input
type="checkbox"
name="agree"
id="agree"
bind:checked=
{values.userAgrees}
/>

{values.userAgrees}
</div>

Let's see the code Svelte compiler generates for us for a simple form. If you study the code generated by the compiler carefully you will find this.


// generated handler for framework input field
function input0_input_handler() {
values.framework = this.value;
$$invalidate(0, values);
}

// generated handler for userAgrees checkbox
function input1_change_handler() {
values.userAgrees = this.checked;
$$invalidate(0, values);
}

// wire up the generated input handlers
listen(input0, "input", /*input0_input_handler*/ ctx[1]),
listen(input1, "change", /*input1_change_handler*/ ctx[2])

There is a lot of internal Svelte magic going on, but the important concept to understand is that default bind directives generate code that roughly matches this JavaScript code.

input0.addEventListener('input', input0_input_handler);
input1.addEventListener('change', input1_change_handler);

As you can see, default two-way bindings are not magical in Svelte, but by using them we are limited to what we can do. Luckily, we have an escape hatch in form of unidirectional bindings.

Unidirectional Bindings#

Instead of outsourcing our input bindings to Svelte we will manage them ourselves. Sure, it's a little more code, but this code give us more granularity and control over what we can accomplish. Now that we know how Svelte compiler does it we can replicate the functionality ourselves.

<script>
const values = {
framework: 'Svelte',
userAgrees: false
}

const inputHandler = e => {
const { name, value } = e.target
values[name] = value
}

const changeHandler = e => {
const { name, checked } = e.target
values[name] = checked
}
</script>

<div>
<input
type="text"
name="framework"
id="framework"
value=
{values.framework}
on:input=
{inputHandler}
/>

{values.framework}
</div>

<div>
<input
type="checkbox"
name="userAgrees"
id="userAgrees"
checked=
{values.userAgrees}
on:change=
{changeHandler}
/>

{values.userAgrees}
</div>

If we look at the generated code we will find this.

function instance($$self, $$props, $$invalidate) {
const values = { framework: "Svelte", userAgrees: false };

const inputHandler = e => {
const { name, value } = e.target;
$$invalidate(0, values[name] = value, values);
};

const changeHandler = e => {
const { name, checked } = e.target;
$$invalidate(0, values[name] = checked, values);
};

return [values, inputHandler, changeHandler];
}

The handlers are almost the same. Only differences is that they are written by us and not generated by the Svelte compiler. This is important as it allows us to gain control of the logic in them.

The Svelte compiler is smart, it will augment our handlers and wrap state in an internal function. That function invalidates state if it has changed and updates the inputs value. Svelte's reactivity in a nutshell.

For the curious, Svelte compiler is using acorn as JavaScript parser in its compiler toolchain.

Calculator Fields#

Now that we've learned both types of input bindings, let's dive into the calculator field generators.

We will keep complexity low. Our requirements is to generate a slug for our blog post based on it's title. For slug generation we will use the slugify package.

Let us also throw in another common business requirement. Slug should not be auto-generated if the slug input field has been touched, meaning interacted with by the user.

Without peeking at the solution below imagine how you would solve it with Svelte's bidirectional bindings. Hard, right?

It's much easier to think in terms of unidirectional bindings for this kind of problem. Here is the solution to the problem.

<script>
import slugify from 'slugify'

const initial = {
title: '',
slug: ''
}

// always work on the copy of original data
let values = { ...initial }

// keep state of touched fields
let touched = {}

const submit = () => console.log(values)

const handleInput = ({ target: { name, value } }) => {
if (name === 'title') {
values.title = value
// only generate slug if the field has not been touched
if (!touched.slug) values.slug = slugify(value, { lower: true })
}

if (name === 'slug') {
values.slug = slugify(value, { lower: true })
}
}

const handleFocus = ({ target: { name } }) => (touched[name] = true)

const reset = () => {
values = { ...initial }
touched = {}
}
</script>

<form on:submit|preventDefault=
{submit} on:reset|preventDefault={reset}>
<div>
<label>
<span>Title</span>
<input
type="text"
name="title"
placeholder="Please provide a title"
value=
{values.title}
on:focus=
{handleFocus}
on:input=
{handleInput}
/>

</label>
</div>
<div>
<label>
<span>Slug</span>
<input
type="text"
name="slug"
placeholder="Choose a slug"
value=
{values.slug}
on:focus=
{handleFocus}
on:input=
{handleInput}
/>

</label>
</div>

<div>
<button type="submit">Save</button>
<button type="reset">Reset</button>
</div>
</form>

The solution is naïve, but that's on purpose. I wanted to demonstrate how to think in unidirectional bindings and how they can help you solve problems that Svelte's bidirectional bindings are not able solve in a good way.

Closing Thoughts#

I noticed that I reach for Svelte's bidirectional bindings less and less because they are limited when it comes to complex forms. Unidirectional bindings give you a lot more control.

Imagine when you have input fields that are truly dependant on each other, a classical temperature converter comes to mind. I have seen solutions using bidirectional bindings and Svelte's stores to solve this, but the code you end up with is messy and unnecessary complex. However, it's a piece of cake when you reach for unidirectional bindings.

One-way bindings are super-flexible, especially when it comes to advanced form validation. If you want to learn more take a look at my Svelte Forms book. It's packed with cool use cases and easy to understand examples!