fix: `defaultValue`/`defaultChecked` in spread attributes

pull/18058/head
paoloricciuti 5 months ago
parent 184eaeface
commit 49511b4a83

@ -472,6 +472,16 @@ function set_attributes(
element.__value = null;
}
}
} else if (is_default && renderer != null) {
// Route through the renderer-aware abstraction so custom renderers
// see defaultValue/defaultChecked as proper attributes
if (name === 'defaultValue') {
set_element_default_value(element, value);
} else {
set_element_default_checked(element, value);
}
// remove it from attributes's cache
if (name in attributes) attributes[name] = UNINITIALIZED;
} else if (
is_default ||
(setters.includes(name) && (is_custom_element || typeof value !== 'string'))

@ -0,0 +1,79 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
test({ assert, target, dispatch_event }) {
const inputs = target.children.filter(
(/** @type {any} */ n) => n.type === 'element' && n.name === 'input'
);
const button = target.children.find(
(/** @type {any} */ n) => n.type === 'element' && n.name === 'button'
);
assert.equal(inputs.length, 4);
assert.ok(button);
// Input 1: direct defaultValue attribute
const input_direct_val = inputs[0];
// Input 2: defaultValue via spread
const input_spread_val = inputs[1];
// Input 3: direct defaultChecked attribute (with `checked` alongside for consistent codegen)
const input_direct_chk = inputs[2];
// Input 4: defaultChecked via spread
const input_spread_chk = inputs[3];
// --- Initial state ---
// Direct defaultValue should appear as a renderer attribute
assert.equal(input_direct_val.attributes['value'], 'fixed');
assert.equal(input_direct_val.attributes['defaultValue'], 'default_val');
// Spread defaultValue should produce the SAME result as direct defaultValue.
// BUG: without fix, spread bypasses the renderer and writes element.defaultValue directly
// so the attribute won't exist on the object-based renderer node
assert.equal(input_spread_val.attributes['value'], 'fixed');
assert.equal(
input_spread_val.attributes['defaultValue'],
'default_val',
'defaultValue via spread should go through renderer.setAttribute, not element.defaultValue'
);
// Direct defaultChecked should appear as a renderer attribute
assert.equal(input_direct_chk.attributes['type'], 'checkbox');
assert.equal(input_direct_chk.attributes['checked'], '');
assert.equal(input_direct_chk.attributes['defaultChecked'], '');
// Spread defaultChecked should also go through the renderer API.
// set_element_default_checked treats true as a boolean attribute (empty string).
assert.equal(input_spread_chk.attributes['type'], 'checkbox');
assert.equal(
input_spread_chk.attributes['defaultChecked'],
'',
'defaultChecked via spread should go through renderer.setAttribute, not element.defaultChecked'
);
// --- After update ---
dispatch_event(button, 'click');
flushSync();
// Direct defaultValue should update
assert.equal(input_direct_val.attributes['defaultValue'], 'new_default');
// Spread defaultValue should also update via renderer
assert.equal(
input_spread_val.attributes['defaultValue'],
'new_default',
'updated defaultValue via spread should go through renderer.setAttribute'
);
// Direct defaultChecked should be removed (false)
assert.equal(input_direct_chk.attributes['defaultChecked'], undefined);
// Spread defaultChecked should also be removed via renderer
assert.equal(
input_spread_chk.attributes['defaultChecked'],
undefined,
'updated defaultChecked=false via spread should go through renderer.removeAttribute'
);
}
});

@ -0,0 +1,16 @@
<script>
let default_val = $state("default_val");
let default_chk = $state(true);
let spread_val = $derived({ defaultValue: default_val });
let spread_chk = $derived({ defaultChecked: default_chk });
</script>
<input value="fixed" defaultValue={default_val} />
<input value="fixed" {...spread_val} />
<input type="checkbox" checked defaultChecked={default_chk} />
<input type="checkbox" checked {...spread_chk} />
<button onclick={() => {
default_val = "new_default";
default_chk = false;
}}>update</button>
Loading…
Cancel
Save