Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add $state.is rune #11613

Merged
merged 4 commits into from
May 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/khaki-monkeys-cry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"svelte": patch
---

feat: add $state.is rune
21 changes: 21 additions & 0 deletions packages/svelte/src/ambient.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,27 @@ declare namespace $state {
*/
export function snapshot<T>(state: T): T;

/**
* Compare two values, one or both of which is a reactive `$state(...)` proxy.
*
* Example:
* ```ts
* <script>
* let foo = $state({});
* let bar = {};
*
* foo.bar = bar;
*
* console.log(foo.bar === bar); // false — `foo.bar` is a reactive proxy
* console.log($state.is(foo.bar, bar)); // true
* </script>
* ```
*
* https://svelte-5-preview.vercel.app/docs/runes#$state.is
*
*/
export function is(a: any, b: any): boolean;

// prevent intellisense from being unhelpful
/** @deprecated */
export const apply: never;
Expand Down
6 changes: 6 additions & 0 deletions packages/svelte/src/compiler/phases/2-analyze/validation.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
1 change: 1 addition & 0 deletions packages/svelte/src/compiler/phases/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export const Runes = /** @type {const} */ ([
'$state',
'$state.frozen',
'$state.snapshot',
'$state.is',
'$props',
'$bindable',
'$derived',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
}
});

Expand Down
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -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;
}
Expand Down
2 changes: 1 addition & 1 deletion packages/svelte/src/internal/client/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
21 changes: 21 additions & 0 deletions packages/svelte/src/internal/client/proxy.js
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { test } from '../../test';

export default test({
async test({ assert, logs }) {
assert.deepEqual(logs, [false, true]);
}
});
10 changes: 10 additions & 0 deletions packages/svelte/tests/runtime-runes/samples/state-is/main.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<script>
/** @type {{ bar?: any }}*/
let foo = $state({});
let bar = {};

foo.bar = bar;

console.log(foo.bar === bar); // false because of the $state proxy
console.log($state.is(foo.bar, bar)); // true
</script>
21 changes: 21 additions & 0 deletions packages/svelte/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2624,6 +2624,27 @@ declare namespace $state {
*/
export function snapshot<T>(state: T): T;

/**
* Compare two values, one or both of which is a reactive `$state(...)` proxy.
*
* Example:
* ```ts
* <script>
* let foo = $state({});
* let bar = {};
*
* foo.bar = bar;
*
* console.log(foo.bar === bar); // false — `foo.bar` is a reactive proxy
* console.log($state.is(foo.bar, bar)); // true
* </script>
* ```
*
* https://svelte-5-preview.vercel.app/docs/runes#$state.is
*
*/
export function is(a: any, b: any): boolean;

// prevent intellisense from being unhelpful
/** @deprecated */
export const apply: never;
Expand Down
1 change: 1 addition & 0 deletions sites/svelte-5-preview/src/lib/autocomplete.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
];
Expand Down
18 changes: 18 additions & 0 deletions sites/svelte-5-preview/src/routes/docs/content/01-api/02-runes.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,24 @@ 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 compare two values, one of which is a reactive `$state(...)` proxy. For this you can use `$state.is(a, b)`:

```svelte
<script>
let foo = $state({});
let bar = {};

foo.bar = bar;

console.log(foo.bar === bar); // false — `foo.bar` is a reactive proxy
console.log($state.is(foo.bar, bar)); // true
</script>
```

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:
Expand Down
Loading