diff --git a/.changeset/fresh-pigs-divide.md b/.changeset/fresh-pigs-divide.md new file mode 100644 index 000000000000..8aa3d74ce9ee --- /dev/null +++ b/.changeset/fresh-pigs-divide.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: ensure props passed to components via mount are updateable diff --git a/packages/svelte/src/internal/client/constants.js b/packages/svelte/src/internal/client/constants.js index 53df86126ac4..bcf30e78db2f 100644 --- a/packages/svelte/src/internal/client/constants.js +++ b/packages/svelte/src/internal/client/constants.js @@ -22,4 +22,5 @@ export const EFFECT_HAS_DERIVED = 1 << 19; export const STATE_SYMBOL = Symbol('$state'); export const STATE_SYMBOL_METADATA = Symbol('$state metadata'); +export const LEGACY_PROPS = Symbol('legacy props'); export const LOADING_ATTR_SYMBOL = Symbol(''); diff --git a/packages/svelte/src/internal/client/reactivity/props.js b/packages/svelte/src/internal/client/reactivity/props.js index dbfb8f0337ab..8fb13f7e2cd0 100644 --- a/packages/svelte/src/internal/client/reactivity/props.js +++ b/packages/svelte/src/internal/client/reactivity/props.js @@ -20,7 +20,13 @@ import { } from '../runtime.js'; import { safe_equals } from './equality.js'; import * as e from '../errors.js'; -import { BRANCH_EFFECT, LEGACY_DERIVED_PROP, ROOT_EFFECT } from '../constants.js'; +import { + BRANCH_EFFECT, + LEGACY_DERIVED_PROP, + LEGACY_PROPS, + ROOT_EFFECT, + STATE_SYMBOL +} from '../constants.js'; import { proxy } from '../proxy.js'; import { capture_store_binding } from './store.js'; import { legacy_mode_flag } from '../../flags/index.js'; @@ -209,6 +215,9 @@ const spread_props_handler = { } }, has(target, key) { + // To prevent a false positive `is_entry_props` in the `prop` function + if (key === STATE_SYMBOL || key === LEGACY_PROPS) return false; + for (let p of target.props) { if (is_function(p)) p = p(); if (p != null && key in p) return true; @@ -282,7 +291,14 @@ export function prop(props, key, flags, fallback) { } else { prop_value = /** @type {V} */ (props[key]); } - var setter = get_descriptor(props, key)?.set; + + // Can be the case when someone does `mount(Component, props)` with `let props = $state({...})` + // or `createClassComponent(Component, props)` + var is_entry_props = STATE_SYMBOL in props || LEGACY_PROPS in props; + + var setter = + get_descriptor(props, key)?.set ?? + (is_entry_props && bindable && key in props ? (v) => (props[key] = v) : undefined); var fallback_value = /** @type {V} */ (fallback); var fallback_dirty = true; diff --git a/packages/svelte/src/internal/client/validate.js b/packages/svelte/src/internal/client/validate.js index eddcb69d9e66..951feee33bdf 100644 --- a/packages/svelte/src/internal/client/validate.js +++ b/packages/svelte/src/internal/client/validate.js @@ -1,4 +1,4 @@ -import { dev_current_component_function, untrack } from './runtime.js'; +import { dev_current_component_function } from './runtime.js'; import { get_descriptor, is_array } from '../shared/utils.js'; import * as e from './errors.js'; import { FILENAME } from '../../constants.js'; @@ -6,15 +6,6 @@ import { render_effect } from './reactivity/effects.js'; import * as w from './warnings.js'; import { capture_store_binding } from './reactivity/store.js'; -/** regex of all html void element names */ -const void_element_names = - /^(?:area|base|br|col|command|embed|hr|img|input|keygen|link|meta|param|source|track|wbr)$/; - -/** @param {string} tag */ -function is_void(tag) { - return void_element_names.test(tag) || tag.toLowerCase() === '!doctype'; -} - /** * @param {() => any} collection * @param {(item: any, index: number) => string} key_fn diff --git a/packages/svelte/src/legacy/legacy-client.js b/packages/svelte/src/legacy/legacy-client.js index c7684570bba7..9e1cd888ad1e 100644 --- a/packages/svelte/src/legacy/legacy-client.js +++ b/packages/svelte/src/legacy/legacy-client.js @@ -1,5 +1,5 @@ /** @import { ComponentConstructorOptions, ComponentType, SvelteComponent, Component } from 'svelte' */ -import { DIRTY, MAYBE_DIRTY } from '../internal/client/constants.js'; +import { DIRTY, LEGACY_PROPS, MAYBE_DIRTY } from '../internal/client/constants.js'; import { user_pre_effect } from '../internal/client/reactivity/effects.js'; import { mutable_source, set } from '../internal/client/reactivity/sources.js'; import { hydrate, mount, unmount } from '../internal/client/render.js'; @@ -89,7 +89,7 @@ class Svelte4Component { }; // Replicate coarse-grained props through a proxy that has a version source for - // each property, which is increment on updates to the property itself. Do not + // each property, which is incremented on updates to the property itself. Do not // use our $state proxy because that one has fine-grained reactivity. const props = new Proxy( { ...(options.props || {}), $$events: {} }, @@ -98,6 +98,9 @@ class Svelte4Component { return get(sources.get(prop) ?? add_source(prop, Reflect.get(target, prop))); }, has(target, prop) { + // Necessary to not throw "invalid binding" validation errors on the component side + if (prop === LEGACY_PROPS) return true; + get(sources.get(prop) ?? add_source(prop, Reflect.get(target, prop))); return Reflect.has(target, prop); }, diff --git a/packages/svelte/tests/runtime-runes/samples/mount-props-updates/_config.js b/packages/svelte/tests/runtime-runes/samples/mount-props-updates/_config.js new file mode 100644 index 000000000000..ff7af2d5244e --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/mount-props-updates/_config.js @@ -0,0 +1,47 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + test({ assert, target }) { + assert.htmlEqual( + target.innerHTML, + // The buz fallback does not propagate back up + ` + foo baz +