From f20cfec73fc8cde4b7446684946e9c923a0dc591 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Tue, 14 May 2024 18:38:10 +0100 Subject: [PATCH 1/4] feat: add $state.is rune --- .changeset/khaki-monkeys-cry.md | 5 +++++ packages/svelte/src/ambient.d.ts | 22 +++++++++++++++++++ .../compiler/phases/2-analyze/validation.js | 6 +++++ .../client/visitors/javascript-runes.js | 11 +++++++++- .../3-transform/server/transform-server.js | 7 ++++++ .../svelte/src/compiler/phases/constants.js | 1 + .../client/dom/elements/bindings/input.js | 5 +++-- .../client/dom/elements/bindings/select.js | 3 ++- packages/svelte/src/internal/client/index.js | 2 +- packages/svelte/src/internal/client/proxy.js | 21 ++++++++++++++++++ .../runtime-runes/samples/state-is/_config.js | 7 ++++++ .../samples/state-is/main.svelte | 10 +++++++++ packages/svelte/types/index.d.ts | 22 +++++++++++++++++++ .../svelte-5-preview/src/lib/autocomplete.js | 1 + .../routes/docs/content/01-api/02-runes.md | 20 +++++++++++++++++ 15 files changed, 138 insertions(+), 5 deletions(-) create mode 100644 .changeset/khaki-monkeys-cry.md create mode 100644 packages/svelte/tests/runtime-runes/samples/state-is/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/state-is/main.svelte diff --git a/.changeset/khaki-monkeys-cry.md b/.changeset/khaki-monkeys-cry.md new file mode 100644 index 000000000000..8135ae94702e --- /dev/null +++ b/.changeset/khaki-monkeys-cry.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +feat: add $state.is rune diff --git a/packages/svelte/src/ambient.d.ts b/packages/svelte/src/ambient.d.ts index 0698530fc3c3..42b1cc6efaa9 100644 --- a/packages/svelte/src/ambient.d.ts +++ b/packages/svelte/src/ambient.d.ts @@ -63,6 +63,28 @@ declare namespace $state { */ export function snapshot(state: T): T; + /** + * To take a check if two reactive from `$state()` are the same, use `$state.is`: + * + * Example: + * ```ts + * + * ``` + * + * https://svelte-5-preview.vercel.app/docs/runes#$state.is + * + */ + export function is(a: unknown, b: unknown): boolean; + // prevent intellisense from being unhelpful /** @deprecated */ export const apply: never; diff --git a/packages/svelte/src/compiler/phases/2-analyze/validation.js b/packages/svelte/src/compiler/phases/2-analyze/validation.js index 00985e838121..544b5a8aa7fd 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/validation.js +++ b/packages/svelte/src/compiler/phases/2-analyze/validation.js @@ -865,6 +865,12 @@ function validate_call_expression(node, scope, path) { e.rune_invalid_arguments_length(node, rune, 'exactly one argument'); } } + + if (rune === '$state.is') { + if (node.arguments.length !== 2) { + e.rune_invalid_arguments_length(node, rune, 'exactly two arguments'); + } + } } /** diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/javascript-runes.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/javascript-runes.js index 9f3192c638c7..e1229a2bedbd 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/javascript-runes.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/javascript-runes.js @@ -209,7 +209,8 @@ export const javascript_visitors_runes = { rune === '$effect.active' || rune === '$effect.root' || rune === '$inspect' || - rune === '$state.snapshot' + rune === '$state.snapshot' || + rune === '$state.is' ) { if (init != null && is_hoistable_function(init)) { const hoistable_function = visit(init); @@ -430,6 +431,14 @@ export const javascript_visitors_runes = { ); } + if (rune === '$state.is') { + return b.call( + '$.is', + /** @type {import('estree').Expression} */ (context.visit(node.arguments[0])), + /** @type {import('estree').Expression} */ (context.visit(node.arguments[1])) + ); + } + if (rune === '$effect.root') { const args = /** @type {import('estree').Expression[]} */ ( node.arguments.map((arg) => context.visit(arg)) diff --git a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js index 12e02eb1d4a5..582cfaf66ae6 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js @@ -778,6 +778,13 @@ const javascript_visitors_runes = { return /** @type {import('estree').Expression} */ (context.visit(node.arguments[0])); } + if (rune === '$state.is') { + return b.call( + 'Object.is', + /** @type {import('estree').Expression} */ (context.visit(node.arguments[0])) + ); + } + if (rune === '$inspect' || rune === '$inspect().with') { return transform_inspect_rune(node, context); } diff --git a/packages/svelte/src/compiler/phases/constants.js b/packages/svelte/src/compiler/phases/constants.js index 54f99995c610..8e3c01139526 100644 --- a/packages/svelte/src/compiler/phases/constants.js +++ b/packages/svelte/src/compiler/phases/constants.js @@ -32,6 +32,7 @@ export const Runes = /** @type {const} */ ([ '$state', '$state.frozen', '$state.snapshot', + '$state.is', '$props', '$bindable', '$derived', diff --git a/packages/svelte/src/internal/client/dom/elements/bindings/input.js b/packages/svelte/src/internal/client/dom/elements/bindings/input.js index 464479496db8..1196a603396f 100644 --- a/packages/svelte/src/internal/client/dom/elements/bindings/input.js +++ b/packages/svelte/src/internal/client/dom/elements/bindings/input.js @@ -3,6 +3,7 @@ import { render_effect, effect } from '../../../reactivity/effects.js'; import { stringify } from '../../../render.js'; import { listen_to_event_and_reset_event } from './shared.js'; import * as e from '../../../errors.js'; +import { get_proxied_value, is } from '../../../proxy.js'; /** * @param {HTMLInputElement} input @@ -95,10 +96,10 @@ export function bind_group(inputs, group_index, input, get_value, update) { if (is_checkbox) { value = value || []; // @ts-ignore - input.checked = value.includes(input.__value); + input.checked = get_proxied_value(value).includes(get_proxied_value(input.__value)); } else { // @ts-ignore - input.checked = input.__value === value; + input.checked = is(input.__value, value); } }); diff --git a/packages/svelte/src/internal/client/dom/elements/bindings/select.js b/packages/svelte/src/internal/client/dom/elements/bindings/select.js index 840be3a7af03..533e8e9af91a 100644 --- a/packages/svelte/src/internal/client/dom/elements/bindings/select.js +++ b/packages/svelte/src/internal/client/dom/elements/bindings/select.js @@ -1,6 +1,7 @@ import { effect } from '../../../reactivity/effects.js'; import { listen_to_event_and_reset_event } from './shared.js'; import { untrack } from '../../../runtime.js'; +import { is } from '../../../proxy.js'; /** * Selects the correct option(s) (depending on whether this is a multiple select) @@ -16,7 +17,7 @@ export function select_option(select, value, mounting) { for (var option of select.options) { var option_value = get_option_value(option); - if (option_value === value) { + if (is(option_value, value)) { option.selected = true; return; } diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index 33eb0c712b1f..8453696e69f1 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -143,7 +143,7 @@ export { validate_prop_bindings } from './validate.js'; export { raf } from './timing.js'; -export { proxy, snapshot } from './proxy.js'; +export { proxy, snapshot, is } from './proxy.js'; export { create_custom_element } from './dom/elements/custom-element.js'; export { child, diff --git a/packages/svelte/src/internal/client/proxy.js b/packages/svelte/src/internal/client/proxy.js index e845da3ba2b0..33e5edbe51ad 100644 --- a/packages/svelte/src/internal/client/proxy.js +++ b/packages/svelte/src/internal/client/proxy.js @@ -337,3 +337,24 @@ if (DEV) { e.state_prototype_fixed(); }; } + +/** + * @param {any} value + */ +export function get_proxied_value(value) { + if (value !== null && typeof value === 'object' && STATE_SYMBOL in value) { + var metadata = value[STATE_SYMBOL]; + if (metadata) { + return metadata.p; + } + } + return value; +} + +/** + * @param {any} a + * @param {any} b + */ +export function is(a, b) { + return Object.is(get_proxied_value(a), get_proxied_value(b)); +} diff --git a/packages/svelte/tests/runtime-runes/samples/state-is/_config.js b/packages/svelte/tests/runtime-runes/samples/state-is/_config.js new file mode 100644 index 000000000000..27aabe821329 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/state-is/_config.js @@ -0,0 +1,7 @@ +import { test } from '../../test'; + +export default test({ + async test({ assert, logs }) { + assert.deepEqual(logs, [false, true]); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/state-is/main.svelte b/packages/svelte/tests/runtime-runes/samples/state-is/main.svelte new file mode 100644 index 000000000000..76a55e7a89f2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/state-is/main.svelte @@ -0,0 +1,10 @@ + diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 3a26e79e91d7..d8cfb6b3a54b 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -2624,6 +2624,28 @@ declare namespace $state { */ export function snapshot(state: T): T; + /** + * To take a check if two reactive from `$state()` are the same, use `$state.is`: + * + * Example: + * ```ts + * + * ``` + * + * https://svelte-5-preview.vercel.app/docs/runes#$state.is + * + */ + export function is(a: unknown, b: unknown): boolean; + // prevent intellisense from being unhelpful /** @deprecated */ export const apply: never; diff --git a/sites/svelte-5-preview/src/lib/autocomplete.js b/sites/svelte-5-preview/src/lib/autocomplete.js index 8e540e9b32a2..ff0c2f741059 100644 --- a/sites/svelte-5-preview/src/lib/autocomplete.js +++ b/sites/svelte-5-preview/src/lib/autocomplete.js @@ -118,6 +118,7 @@ const runes = [ { snippet: '$bindable()', test: is_bindable }, { snippet: '$effect.root(() => {\n\t${}\n})' }, { snippet: '$state.snapshot(${})' }, + { snippet: '$state.is(${})' }, { snippet: '$effect.active()' }, { snippet: '$inspect(${});', test: is_statement } ]; diff --git a/sites/svelte-5-preview/src/routes/docs/content/01-api/02-runes.md b/sites/svelte-5-preview/src/routes/docs/content/01-api/02-runes.md index ae2ccd510c0b..954b7774ad4b 100644 --- a/sites/svelte-5-preview/src/routes/docs/content/01-api/02-runes.md +++ b/sites/svelte-5-preview/src/routes/docs/content/01-api/02-runes.md @@ -112,6 +112,26 @@ This is handy when you want to pass some state to an external library or API tha > Note that `$state.snapshot` will clone the data when removing reactivity. If the value passed isn't a `$state` proxy, it will be returned as-is. +## `$state.is` + +Sometimes you might need to deal with the equality of two values, where one of the objects might have been wrapped in a with a `$state` proxy, +you can use `$state.is` to check if the two values are the same. + +```svelte + +``` + +This is handy when you might want to check if the object exists within a deeply reactive object/array. + ## `$derived` Derived state is declared with the `$derived` rune: From 2ab20e0397790a3e4dd4f65164450275051d2d83 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Tue, 14 May 2024 18:42:51 +0100 Subject: [PATCH 2/4] fix type --- packages/svelte/src/ambient.d.ts | 2 +- packages/svelte/types/index.d.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/svelte/src/ambient.d.ts b/packages/svelte/src/ambient.d.ts index 42b1cc6efaa9..f28e5f64a3dc 100644 --- a/packages/svelte/src/ambient.d.ts +++ b/packages/svelte/src/ambient.d.ts @@ -83,7 +83,7 @@ declare namespace $state { * https://svelte-5-preview.vercel.app/docs/runes#$state.is * */ - export function is(a: unknown, b: unknown): boolean; + export function is(a: any, b: any): boolean; // prevent intellisense from being unhelpful /** @deprecated */ diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index d8cfb6b3a54b..f1a9b735ffef 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -2644,7 +2644,7 @@ declare namespace $state { * https://svelte-5-preview.vercel.app/docs/runes#$state.is * */ - export function is(a: unknown, b: unknown): boolean; + export function is(a: any, b: any): boolean; // prevent intellisense from being unhelpful /** @deprecated */ From 122f44b69df9afc921a97dc4124ba4a830d099fd Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 14 May 2024 15:34:55 -0400 Subject: [PATCH 3/4] tweak docs --- packages/svelte/src/ambient.d.ts | 13 ++++++------- packages/svelte/types/index.d.ts | 13 ++++++------- .../src/routes/docs/content/01-api/02-runes.md | 14 ++++++-------- 3 files changed, 18 insertions(+), 22 deletions(-) diff --git a/packages/svelte/src/ambient.d.ts b/packages/svelte/src/ambient.d.ts index f28e5f64a3dc..68d881ced09d 100644 --- a/packages/svelte/src/ambient.d.ts +++ b/packages/svelte/src/ambient.d.ts @@ -64,19 +64,18 @@ declare namespace $state { export function snapshot(state: T): T; /** - * To take a check if two reactive from `$state()` are the same, use `$state.is`: + * Compare two values, one or both of which is a reactive `$state(...)` proxy. * * Example: * ```ts * * ``` * diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index f1a9b735ffef..39695c7bade0 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -2625,19 +2625,18 @@ declare namespace $state { export function snapshot(state: T): T; /** - * To take a check if two reactive from `$state()` are the same, use `$state.is`: + * Compare two values, one or both of which is a reactive `$state(...)` proxy. * * Example: * ```ts * * ``` * diff --git a/sites/svelte-5-preview/src/routes/docs/content/01-api/02-runes.md b/sites/svelte-5-preview/src/routes/docs/content/01-api/02-runes.md index 954b7774ad4b..b9b75d2db1a2 100644 --- a/sites/svelte-5-preview/src/routes/docs/content/01-api/02-runes.md +++ b/sites/svelte-5-preview/src/routes/docs/content/01-api/02-runes.md @@ -114,19 +114,17 @@ This is handy when you want to pass some state to an external library or API tha ## `$state.is` -Sometimes you might need to deal with the equality of two values, where one of the objects might have been wrapped in a with a `$state` proxy, -you can use `$state.is` to check if the two values are the same. +Sometimes you might need to compare two values, one of which is a reactive `$state(...)` proxy. For this you can use `$state.is(a, b)`: ```svelte ``` From c915f593fb2e70a52a1d4e87c0da4013181d2f22 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 14 May 2024 15:37:07 -0400 Subject: [PATCH 4/4] may as well update the test case to match the docs --- .../tests/runtime-runes/samples/state-is/main.svelte | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/svelte/tests/runtime-runes/samples/state-is/main.svelte b/packages/svelte/tests/runtime-runes/samples/state-is/main.svelte index 76a55e7a89f2..ff23de439a3c 100644 --- a/packages/svelte/tests/runtime-runes/samples/state-is/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/state-is/main.svelte @@ -1,10 +1,10 @@