diff --git a/documentation/docs/02-runes/06-$bindable.md b/documentation/docs/02-runes/06-$bindable.md index 14bc8ddbec05..c12c2bf4903e 100644 --- a/documentation/docs/02-runes/06-$bindable.md +++ b/documentation/docs/02-runes/06-$bindable.md @@ -33,7 +33,7 @@ Now, a component that uses `` can add the [`bind:`](bind) directive ```svelte -/// App.svelte +/// file: App.svelte - + ``` Components also support `bind:this`, allowing you to interact with component instances programmatically. diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 948a9147784c..ab190e1cc2af 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,21 @@ # svelte +## 5.20.2 + +### Patch Changes + +- chore: remove unused `options.uid` in `render` ([#15302](https://github.com/sveltejs/svelte/pull/15302)) + +- fix: do not warn for `binding_property_non_reactive` if binding is a store in an each ([#15318](https://github.com/sveltejs/svelte/pull/15318)) + +- fix: prevent writable store value from becoming a proxy when reassigning using $-prefix ([#15283](https://github.com/sveltejs/svelte/pull/15283)) + +- fix: `muted` reactive without `bind` and select/autofocus attributes working with function calls ([#15326](https://github.com/sveltejs/svelte/pull/15326)) + +- fix: ensure input elements and elements with `dir` attribute are marked as non-static ([#15259](https://github.com/sveltejs/svelte/pull/15259)) + +- fix: fire delegated events on target even it was disabled in the meantime ([#15319](https://github.com/sveltejs/svelte/pull/15319)) + ## 5.20.1 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 326124383267..579660f9d771 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -2,7 +2,7 @@ "name": "svelte", "description": "Cybernetically enhanced web apps", "license": "MIT", - "version": "5.20.1", + "version": "5.20.2", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts b/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts index db4adf451c2e..63fe3223cf7d 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts +++ b/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts @@ -45,6 +45,7 @@ export interface ComponentClientTransformState extends ClientTransformState { readonly hoisted: Array; readonly events: Set; readonly is_instance: boolean; + readonly store_to_invalidate?: string; /** Stuff that happens before the render effect(s) */ readonly init: Statement[]; diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js index 0c70f7e00cda..a8c615af936f 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js @@ -118,6 +118,7 @@ function build_assignment(operator, left, right, context) { binding.kind !== 'prop' && binding.kind !== 'bindable_prop' && binding.kind !== 'raw_state' && + binding.kind !== 'store_sub' && context.state.analysis.runes && should_proxy(right, context.state.scope) && is_non_coercive_operator(operator) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js index 24a696e7d215..629cacda0148 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js @@ -143,7 +143,8 @@ export function EachBlock(node, context) { const child_state = { ...context.state, - transform: { ...context.state.transform } + transform: { ...context.state.transform }, + store_to_invalidate }; /** The state used when generating the key function, if necessary */ diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js index 98036aa9b609..018bdacc5ecd 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js @@ -29,7 +29,8 @@ import { build_render_statement, build_template_chunk, build_update_assignment, - get_expression_id + get_expression_id, + memoize_expression } from './shared/utils.js'; import { visit_event_attribute } from './shared/events.js'; @@ -532,18 +533,30 @@ function build_element_attribute_update_assignment( const is_svg = context.state.metadata.namespace === 'svg' || element.name === 'svg'; const is_mathml = context.state.metadata.namespace === 'mathml'; + const is_autofocus = name === 'autofocus'; + let { value, has_state } = build_attribute_value(attribute.value, context, (value, metadata) => - metadata.has_call ? get_expression_id(state, value) : value + metadata.has_call + ? // if it's autofocus we will not add this to a template effect so we don't want to get the expression id + // but separately memoize the expression + is_autofocus + ? memoize_expression(state, value) + : get_expression_id(state, value) + : value ); - if (name === 'autofocus') { + if (is_autofocus) { state.init.push(b.stmt(b.call('$.autofocus', node_id, value))); return false; } // Special case for Firefox who needs it set as a property in order to work if (name === 'muted') { - state.init.push(b.stmt(b.assignment('=', b.member(node_id, b.id('muted')), value))); + if (!has_state) { + state.init.push(b.stmt(b.assignment('=', b.member(node_id, b.id('muted')), value))); + return false; + } + state.update.push(b.stmt(b.assignment('=', b.member(node_id, b.id('muted')), value))); return false; } @@ -660,8 +673,18 @@ function build_custom_element_attribute_update_assignment(node_id, attribute, co */ function build_element_special_value_attribute(element, node_id, attribute, context) { const state = context.state; + const is_select_with_value = + // attribute.metadata.dynamic would give false negatives because even if the value does not change, + // the inner options could still change, so we need to always treat it as reactive + element === 'select' && attribute.value !== true && !is_text_attribute(attribute); + const { value, has_state } = build_attribute_value(attribute.value, context, (value, metadata) => - metadata.has_call ? get_expression_id(state, value) : value + metadata.has_call + ? // if is a select with value we will also invoke `init_select` which need a reference before the template effect so we memoize separately + is_select_with_value + ? memoize_expression(context.state, value) + : get_expression_id(state, value) + : value ); const inner_assignment = b.assignment( @@ -674,11 +697,6 @@ function build_element_special_value_attribute(element, node_id, attribute, cont ) ); - const is_select_with_value = - // attribute.metadata.dynamic would give false negatives because even if the value does not change, - // the inner options could still change, so we need to always treat it as reactive - element === 'select' && attribute.value !== true && !is_text_attribute(attribute); - const update = b.stmt( is_select_with_value ? b.sequence([ diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/fragment.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/fragment.js index 5bc3041ca453..f076d7c11ea9 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/fragment.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/fragment.js @@ -144,6 +144,17 @@ function is_static_element(node, state) { return false; } + if (attribute.name === 'dir') { + return false; + } + + if ( + ['input', 'textarea'].includes(node.name) && + ['value', 'checked'].includes(attribute.name) + ) { + return false; + } + if (node.name === 'option' && attribute.name === 'value') { return false; } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js index 9214a13c94ca..ca5094f455c6 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js @@ -309,12 +309,16 @@ export function validate_binding(state, binding, expression) { const loc = locator(binding.start); + const obj = /** @type {Expression} */ (expression.object); + state.init.push( b.stmt( b.call( '$.validate_binding', b.literal(state.analysis.source.slice(binding.start, binding.end)), - b.thunk(/** @type {Expression} */ (expression.object)), + b.thunk( + state.store_to_invalidate ? b.sequence([b.call('$.mark_store_binding'), obj]) : obj + ), b.thunk( /** @type {Expression} */ ( expression.computed diff --git a/packages/svelte/src/internal/client/dom/elements/events.js b/packages/svelte/src/internal/client/dom/elements/events.js index 363b8e1ed501..25ece5f569d7 100644 --- a/packages/svelte/src/internal/client/dom/elements/events.js +++ b/packages/svelte/src/internal/client/dom/elements/events.js @@ -237,7 +237,13 @@ export function handle_event_propagation(event) { // @ts-expect-error var delegated = current_target['__' + event_name]; - if (delegated !== undefined && !(/** @type {any} */ (current_target).disabled)) { + if ( + delegated !== undefined && + (!(/** @type {any} */ (current_target).disabled) || + // DOM could've been updated already by the time this is reached, so we check this as well + // -> the target could not have been disabled because it emits the event in the first place + event.target === current_target) + ) { if (is_array(delegated)) { var [fn, ...data] = delegated; fn.apply(current_target, [event, ...data]); diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js index 728f2ebc2a3c..e8ffeed2fef5 100644 --- a/packages/svelte/src/internal/server/index.js +++ b/packages/svelte/src/internal/server/index.js @@ -97,11 +97,11 @@ function props_id_generator() { * Takes a component and returns an object with `body` and `head` properties on it, which you can use to populate the HTML when server-rendering your app. * @template {Record} Props * @param {import('svelte').Component | ComponentType>} component - * @param {{ props?: Omit; context?: Map, uid?: () => string }} [options] + * @param {{ props?: Omit; context?: Map }} [options] * @returns {RenderOutput} */ export function render(component, options = {}) { - const uid = options.uid ?? props_id_generator(); + const uid = props_id_generator(); /** @type {Payload} */ const payload = { out: '', diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 9f1873efc7e8..0803fae7363f 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.20.1'; +export const VERSION = '5.20.2'; export const PUBLIC_VERSION = '5'; diff --git a/packages/svelte/tests/hydration/samples/element-dir-attribute-sibling/_config.js b/packages/svelte/tests/hydration/samples/element-dir-attribute-sibling/_config.js new file mode 100644 index 000000000000..e3c629aef989 --- /dev/null +++ b/packages/svelte/tests/hydration/samples/element-dir-attribute-sibling/_config.js @@ -0,0 +1,9 @@ +import { test } from '../../test'; + +export default test({ + test(assert, target) { + const p = target.querySelector('p'); + + assert.equal(p?.dir, 'rtl'); + } +}); diff --git a/packages/svelte/tests/hydration/samples/element-dir-attribute-sibling/main.svelte b/packages/svelte/tests/hydration/samples/element-dir-attribute-sibling/main.svelte new file mode 100644 index 000000000000..802edc0feeb6 --- /dev/null +++ b/packages/svelte/tests/hydration/samples/element-dir-attribute-sibling/main.svelte @@ -0,0 +1 @@ +

text

. diff --git a/packages/svelte/tests/hydration/samples/input-checked-attribute-sibling/_config.js b/packages/svelte/tests/hydration/samples/input-checked-attribute-sibling/_config.js new file mode 100644 index 000000000000..31ec66fc8857 --- /dev/null +++ b/packages/svelte/tests/hydration/samples/input-checked-attribute-sibling/_config.js @@ -0,0 +1,9 @@ +import { test } from '../../test'; + +export default test({ + test(assert, target) { + const input = target.querySelector('input'); + + assert.equal(input?.checked, true); + } +}); diff --git a/packages/svelte/tests/hydration/samples/input-checked-attribute-sibling/_expected.html b/packages/svelte/tests/hydration/samples/input-checked-attribute-sibling/_expected.html new file mode 100644 index 000000000000..bcd53f878357 --- /dev/null +++ b/packages/svelte/tests/hydration/samples/input-checked-attribute-sibling/_expected.html @@ -0,0 +1 @@ +. diff --git a/packages/svelte/tests/hydration/samples/input-checked-attribute-sibling/main.svelte b/packages/svelte/tests/hydration/samples/input-checked-attribute-sibling/main.svelte new file mode 100644 index 000000000000..db3eae870f7c --- /dev/null +++ b/packages/svelte/tests/hydration/samples/input-checked-attribute-sibling/main.svelte @@ -0,0 +1 @@ +. diff --git a/packages/svelte/tests/preprocess/test.ts b/packages/svelte/tests/preprocess/test.ts index 08f237e28667..81030a634633 100644 --- a/packages/svelte/tests/preprocess/test.ts +++ b/packages/svelte/tests/preprocess/test.ts @@ -25,7 +25,7 @@ const { test, run } = suite(async (config, cwd) => { fs.writeFileSync(`${cwd}/_actual.html.map`, JSON.stringify(result.map, null, 2)); } - expect(result.code).toMatchFileSnapshot(`${cwd}/output.svelte`); + await expect(result.code).toMatchFileSnapshot(`${cwd}/output.svelte`); expect(result.dependencies).toEqual(config.dependencies || []); diff --git a/packages/svelte/tests/runtime-runes/samples/autofocus-with-call/_config.js b/packages/svelte/tests/runtime-runes/samples/autofocus-with-call/_config.js new file mode 100644 index 000000000000..0597c2fda899 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/autofocus-with-call/_config.js @@ -0,0 +1,7 @@ +import { test } from '../../test'; + +export default test({ + async test({ assert, errors }) { + assert.deepEqual(errors, []); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/autofocus-with-call/main.svelte b/packages/svelte/tests/runtime-runes/samples/autofocus-with-call/main.svelte new file mode 100644 index 000000000000..cb3804af34e4 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/autofocus-with-call/main.svelte @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/each-bind-store-no-warning/_config.js b/packages/svelte/tests/runtime-runes/samples/each-bind-store-no-warning/_config.js new file mode 100644 index 000000000000..dba2e856500c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/each-bind-store-no-warning/_config.js @@ -0,0 +1,10 @@ +import { test } from '../../test'; + +export default test({ + compileOptions: { + dev: true + }, + async test({ assert, warnings }) { + assert.deepEqual(warnings, []); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/each-bind-store-no-warning/main.svelte b/packages/svelte/tests/runtime-runes/samples/each-bind-store-no-warning/main.svelte new file mode 100644 index 000000000000..f927bf079a1d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/each-bind-store-no-warning/main.svelte @@ -0,0 +1,10 @@ + + + +{#each $array as item} +
+{/each} diff --git a/packages/svelte/tests/runtime-runes/samples/muted-without-bind-works/_config.js b/packages/svelte/tests/runtime-runes/samples/muted-without-bind-works/_config.js new file mode 100644 index 000000000000..cc4dfb37f098 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/muted-without-bind-works/_config.js @@ -0,0 +1,13 @@ +import { flushSync } from 'svelte'; +import { ok, test } from '../../test'; + +export default test({ + async test({ assert, target, logs }) { + const btn = target.querySelector('button'); + ok(btn); + flushSync(() => { + btn.click(); + }); + assert.deepEqual(logs, [true]); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/muted-without-bind-works/main.svelte b/packages/svelte/tests/runtime-runes/samples/muted-without-bind-works/main.svelte new file mode 100644 index 000000000000..646334c1ece3 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/muted-without-bind-works/main.svelte @@ -0,0 +1,13 @@ + + + + + diff --git a/packages/svelte/tests/runtime-runes/samples/select-value-with-call/_config.js b/packages/svelte/tests/runtime-runes/samples/select-value-with-call/_config.js new file mode 100644 index 000000000000..0597c2fda899 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/select-value-with-call/_config.js @@ -0,0 +1,7 @@ +import { test } from '../../test'; + +export default test({ + async test({ assert, errors }) { + assert.deepEqual(errors, []); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/select-value-with-call/main.svelte b/packages/svelte/tests/runtime-runes/samples/select-value-with-call/main.svelte new file mode 100644 index 000000000000..b1d60ecf6d9f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/select-value-with-call/main.svelte @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/store-reassign-object/_config.js b/packages/svelte/tests/runtime-runes/samples/store-reassign-object/_config.js new file mode 100644 index 000000000000..f9a329889d21 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/store-reassign-object/_config.js @@ -0,0 +1,7 @@ +import { test } from '../../test'; + +export default test({ + async test({ target, assert }) { + assert.htmlEqual(target.innerHTML, `

bar

`); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/store-reassign-object/main.svelte b/packages/svelte/tests/runtime-runes/samples/store-reassign-object/main.svelte new file mode 100644 index 000000000000..ecffbb2d837c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/store-reassign-object/main.svelte @@ -0,0 +1,11 @@ + + +

{clone.name}

pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy