fix: ensure bound input content is resumed on hydration (#11986)

* fix: ensure bound input content is resumed on hydration

* fix: ensure bound input content is resumed on hydration

* Update packages/svelte/src/internal/client/dom/elements/bindings/input.js

Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com>

* fix: ensure bound input content is resumed on hydration

* fix: ensure bound input content is resumed on hydration

* Update packages/svelte/src/internal/client/dom/elements/bindings/input.js

Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com>

* add test

* add test

* add test

* add test

* Update packages/svelte/src/internal/client/dom/elements/bindings/input.js

Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com>

* add test

* add test

* newlines between multi-line blocks, let the code breathe

---------

Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com>
Co-authored-by: Rich Harris <rich.harris@vercel.com>
pull/12009/head
Dominic Gannaway 1 year ago committed by GitHub
parent f1c9edcc63
commit 0d51dbae32
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: ensure bound input content is resumed on hydration

@ -4,6 +4,7 @@ import { listen_to_event_and_reset_event } from './shared.js';
import * as e from '../../../errors.js';
import { get_proxied_value, is } from '../../../proxy.js';
import { queue_micro_task } from '../../task.js';
import { hydrating } from '../../hydration.js';
/**
* @param {HTMLInputElement} input
@ -29,8 +30,12 @@ export function bind_value(input, get_value, update) {
var value = get_value();
// @ts-ignore
input.__value = value;
// If we are hydrating and the value has since changed, then use the update value
// from the input instead.
if (hydrating && input.defaultValue !== input.value) {
update(input.value);
return;
}
if (is_numberlike_input(input) && value === to_number(input.value)) {
// handles 0 vs 00 case (see https://github.com/sveltejs/svelte/issues/9959)
@ -60,6 +65,9 @@ export function bind_group(inputs, group_index, input, get_value, update) {
var is_checkbox = input.getAttribute('type') === 'checkbox';
var binding_group = inputs;
// needs to be let or related code isn't treeshaken out if it's always false
let hydration_mismatch = false;
if (group_index !== null) {
for (var index of group_index) {
var group = binding_group;
@ -94,6 +102,13 @@ export function bind_group(inputs, group_index, input, get_value, update) {
render_effect(() => {
var value = get_value();
// If we are hydrating and the value has since changed, then use the update value
// from the input instead.
if (hydrating && input.defaultChecked !== input.checked) {
hydration_mismatch = true;
return;
}
if (is_checkbox) {
value = value || [];
// @ts-ignore
@ -115,6 +130,20 @@ export function bind_group(inputs, group_index, input, get_value, update) {
queue_micro_task(() => {
// necessary to maintain binding group order in all insertion scenarios. TODO optimise
binding_group.sort((a, b) => (a.compareDocumentPosition(b) === 4 ? -1 : 1));
if (hydration_mismatch) {
var value;
if (is_checkbox) {
value = get_binding_group_value(binding_group, value, input.checked);
} else {
var hydration_input = binding_group.find((input) => input.checked);
// @ts-ignore
value = hydration_input?.__value;
}
update(value);
}
});
}

@ -59,6 +59,7 @@ export interface RuntimeTest<Props extends Record<string, any> = Record<string,
};
logs: any[];
warnings: any[];
hydrate: Function;
}) => void | Promise<void>;
test_ssr?: (args: { assert: Assert }) => void | Promise<void>;
accessors?: boolean;
@ -103,6 +104,10 @@ export function runtime_suite(runes: boolean) {
if (config.skip_mode?.includes('hydrate')) return true;
}
if (variant === 'dom' && config.skip_mode?.includes('client')) {
return 'no-test';
}
if (variant === 'ssr') {
if (
(config.mode && !config.mode.includes('server')) ||
@ -161,6 +166,7 @@ async function run_test_variant(
let logs: string[] = [];
let warnings: string[] = [];
let manual_hydrate = false;
{
// use some crude static analysis to determine if logs/warnings are intercepted.
@ -180,6 +186,10 @@ async function run_test_variant(
console.log = (...args) => logs.push(...args);
}
if (str.slice(0, i).includes('hydrate')) {
manual_hydrate = true;
}
if (str.slice(0, i).includes('warnings') || config.warnings) {
// eslint-disable-next-line no-console
console.warn = (...args) => {
@ -297,17 +307,30 @@ async function run_test_variant(
let instance: any;
let props: any;
let hydrate_fn: Function = () => {
throw new Error('Ensure dom mode is skipped');
};
if (runes) {
props = proxy({ ...(config.props || {}) });
const render = variant === 'hydrate' ? hydrate : mount;
instance = render(mod.default, {
target,
props,
intro: config.intro,
recover: config.recover ?? false
});
if (manual_hydrate) {
hydrate_fn = () => {
instance = hydrate(mod.default, {
target,
props,
intro: config.intro,
recover: config.recover ?? false
});
};
} else {
const render = variant === 'hydrate' ? hydrate : mount;
instance = render(mod.default, {
target,
props,
intro: config.intro,
recover: config.recover ?? false
});
}
} else {
instance = createClassComponent({
component: mod.default,
@ -357,7 +380,8 @@ async function run_test_variant(
raf,
compileOptions,
logs,
warnings
warnings,
hydrate: hydrate_fn
});
}

@ -0,0 +1,18 @@
import { test } from '../../test';
export default test({
skip_mode: ['client'],
test({ assert, target, hydrate }) {
const inputs = /** @type {NodeListOf<HTMLInputElement>} */ (target.querySelectorAll('input'));
inputs[1].checked = true;
inputs[1].dispatchEvent(new window.Event('change'));
// Hydration shouldn't reset the value to 1
hydrate();
assert.htmlEqual(
target.innerHTML,
'<input name="foo" type="radio" value="1"><input name="foo" type="radio" value="2"><input name="foo" type="radio" value="3">\n2'
);
}
});

@ -0,0 +1,9 @@
<script>
let value = $state(1);
</script>
{#each [1, 2, 3] as number}
<input type="radio" name="foo" value={number} bind:group={value}>
{/each}
{value}

@ -0,0 +1,15 @@
import { test } from '../../test';
export default test({
skip_mode: ['client'],
test({ assert, target, hydrate }) {
const input = /** @type {HTMLInputElement} */ (target.querySelector('input'));
input.value = 'foo';
input.dispatchEvent(new window.Event('input'));
// Hydration shouldn't reset the value to empty
hydrate();
assert.htmlEqual(target.innerHTML, '<input type="text">\nfoo');
}
});

@ -0,0 +1,6 @@
<script>
let value = $state('');
</script>
<input type="text" bind:value={value}>
{value}

@ -4,8 +4,11 @@ import App from './App.svelte';
const root = document.getElementById('root')!;
const render = root.firstChild?.nextSibling ? hydrate : mount;
const component = render(App, {
target: document.getElementById('root')!
});
// @ts-ignore
window.unmount = () => unmount(component);
setTimeout(() => {
const component = render(App, {
target: document.getElementById('root')!
});
// @ts-ignore
window.unmount = () => unmount(component);
}, 2000)

Loading…
Cancel
Save