18 May 2024

Two-way binding in Svelte

Probably every time I’ve implemented anything slightly complex in Svelte I’ve wondered if React’s one-way binding is the way to go after all. I don’t think it is, but you do have to do things a certain way in Svelte to avoid getting into trouble with two-way binding.

Using a component that provides a numeric form input and converts the value to a number as an example, you can quickly get yourself tangled up in an infinite loop or other weird bugs if you try and get the following behaviour:

  • The native input value is a stringified version of the number in the component’s value prop.

  • The component’s value prop, via two-way binding and reactive assignment, is the input value converted to a number.

The way to get this working is to only propagate the updates in one direction at a time: while the user is focusing the input, update the numeric value based on the input value; and while not focused, update the input value based on the numeric value.

<script>
export let value;

let inputValue;
let focused = false;

$: if (!focused) updateInputValue(value);

$: if (focused) updateValue(inputValue);

function parse(s) {
	s = s.replace(/D/g, "");
	
	if (!s.match(/^d+$/)) {
		return null;
	}
	
	return Number(s);
}

function updateValue(inputValue) {
	value = parse(inputValue);
}

function updateInputValue(value) {
	inputValue = value === null ? "" : String(value);
}

function onFocus() {
	focused = true;
}

function onBlur() {
	focused = false;
	
	updateInputValue();
}
</script>

<input
	bind:value={inputValue}
	on:blur={onBlur}
	on:focus={onFocus}
/>

This way there is no circular dependency, and you don’t mess with the input while the user is typing in it.

Note: I haven’t tried this with Linux middle-click pasting, but a quick test shows that this does cause a focus event to fire before paste, so I would bet that it works.