From 6575151e9a3366cb85029d85fb50281561d01013 Mon Sep 17 00:00:00 2001 From: paoloricciuti Date: Sat, 4 May 2024 18:26:54 +0200 Subject: [PATCH] fix: fine grained hydration attribute removal --- .changeset/loud-numbers-flow.md | 5 +++ .../3-transform/client/visitors/template.js | 31 ++++++++++++------- .../client/dom/elements/attributes.js | 7 +++-- .../_config.js | 12 +++++++ .../main.svelte | 5 +++ 5 files changed, 45 insertions(+), 15 deletions(-) create mode 100644 .changeset/loud-numbers-flow.md create mode 100644 packages/svelte/tests/runtime-browser/samples/fine-grained-hydration-clean-attr/_config.js create mode 100644 packages/svelte/tests/runtime-browser/samples/fine-grained-hydration-clean-attr/main.svelte diff --git a/.changeset/loud-numbers-flow.md b/.changeset/loud-numbers-flow.md new file mode 100644 index 0000000000..e04df7d6e5 --- /dev/null +++ b/.changeset/loud-numbers-flow.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +fix: fine grained hydration attribute removal diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js index b6fe9649eb..c6afeeab61 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js @@ -1838,7 +1838,10 @@ export const template_visitors = { const lets = []; const is_custom_element = is_custom_element_node(node); - let needs_input_reset = false; + /** + * @type {import('estree').Expression[]} + */ + let needs_input_reset = []; let needs_content_reset = false; /** @type {import('#compiler').BindDirective | null} */ @@ -1860,11 +1863,11 @@ export const template_visitors = { for (const attribute of node.attributes) { if (attribute.type === 'Attribute') { attributes.push(attribute); - if ( - (attribute.name === 'value' || attribute.name === 'checked') && - !is_text_attribute(attribute) - ) { - needs_input_reset = true; + if (attribute.name === 'value' && !is_text_attribute(attribute)) { + needs_input_reset.push(b.literal('value')); + needs_content_reset = true; + } else if (attribute.name === 'checked' && !is_text_attribute(attribute)) { + needs_input_reset.push(b.literal('checked')); needs_content_reset = true; } else if ( attribute.name === 'contenteditable' && @@ -1875,7 +1878,7 @@ export const template_visitors = { } } else if (attribute.type === 'SpreadAttribute') { attributes.push(attribute); - needs_input_reset = true; + needs_input_reset = [b.literal('value'), b.literal('checked')]; needs_content_reset = true; } else if (attribute.type === 'ClassDirective') { class_directives.push(attribute); @@ -1887,11 +1890,11 @@ export const template_visitors = { if (attribute.type === 'BindDirective') { if (attribute.name === 'group' || attribute.name === 'checked') { needs_special_value_handling = true; - needs_input_reset = true; + needs_input_reset.push(b.literal('checked')); } else if (attribute.name === 'value') { value_binding = attribute; needs_content_reset = true; - needs_input_reset = true; + needs_input_reset.push(b.literal('value')); } else if ( attribute.name === 'innerHTML' || attribute.name === 'innerText' || @@ -1907,7 +1910,7 @@ export const template_visitors = { if (child_metadata.namespace === 'foreign') { // input/select etc could mean something completely different in foreign namespace, so don't special-case them needs_content_reset = false; - needs_input_reset = false; + needs_input_reset = []; needs_special_value_handling = false; value_binding = null; } @@ -1916,8 +1919,12 @@ export const template_visitors = { child_metadata.bound_contenteditable = true; } - if (needs_input_reset && (node.name === 'input' || node.name === 'select')) { - context.state.init.push(b.stmt(b.call('$.remove_input_attr_defaults', context.state.node))); + if (needs_input_reset.length > 0 && (node.name === 'input' || node.name === 'select')) { + context.state.init.push( + b.stmt( + b.call('$.remove_input_attr_defaults', context.state.node, b.array(needs_input_reset)) + ) + ); } if (needs_content_reset && node.name === 'textarea') { diff --git a/packages/svelte/src/internal/client/dom/elements/attributes.js b/packages/svelte/src/internal/client/dom/elements/attributes.js index 5e39228f2b..edd20df4cb 100644 --- a/packages/svelte/src/internal/client/dom/elements/attributes.js +++ b/packages/svelte/src/internal/client/dom/elements/attributes.js @@ -14,10 +14,11 @@ import * as w from '../../warnings.js'; * @param {HTMLInputElement | HTMLSelectElement} dom * @returns {void} */ -export function remove_input_attr_defaults(dom) { +export function remove_input_attr_defaults(dom, to_remove = ['value', 'checked']) { if (hydrating) { - set_attribute(dom, 'value', null); - set_attribute(dom, 'checked', null); + for (const attr of to_remove) { + set_attribute(dom, attr, null); + } } } diff --git a/packages/svelte/tests/runtime-browser/samples/fine-grained-hydration-clean-attr/_config.js b/packages/svelte/tests/runtime-browser/samples/fine-grained-hydration-clean-attr/_config.js new file mode 100644 index 0000000000..db03102d8d --- /dev/null +++ b/packages/svelte/tests/runtime-browser/samples/fine-grained-hydration-clean-attr/_config.js @@ -0,0 +1,12 @@ +import { test } from '../../assert'; + +export default test({ + html: ``, + mode: ['server'], + test({ window, assert, mod }) { + assert.htmlEqual( + window.document.body.innerHTML, + `` + ); + } +}); diff --git a/packages/svelte/tests/runtime-browser/samples/fine-grained-hydration-clean-attr/main.svelte b/packages/svelte/tests/runtime-browser/samples/fine-grained-hydration-clean-attr/main.svelte new file mode 100644 index 0000000000..9dc489489b --- /dev/null +++ b/packages/svelte/tests/runtime-browser/samples/fine-grained-hydration-clean-attr/main.svelte @@ -0,0 +1,5 @@ + + +