Skip to content

Commit d7876af

Browse files
committed
feat: add onchange option to $state
1 parent de94159 commit d7876af

File tree

21 files changed

+185
-39
lines changed

21 files changed

+185
-39
lines changed

.changeset/red-rules-share.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': minor
3+
---
4+
5+
feat: add `onchange` option to `$state`

packages/svelte/src/ambient.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ declare module '*.svelte' {
2020
*
2121
* @param initial The initial value
2222
*/
23+
declare function $state<T>(initial?: T, options?: import('svelte').StateOptions): T;
2324
declare function $state<T>(initial: T): T;
2425
declare function $state<T>(): T | undefined;
2526

packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,8 @@ export function CallExpression(node, context) {
8787

8888
if ((rune === '$derived' || rune === '$derived.by') && node.arguments.length !== 1) {
8989
e.rune_invalid_arguments_length(node, rune, 'exactly one argument');
90-
} else if (rune === '$state' && node.arguments.length > 1) {
91-
e.rune_invalid_arguments_length(node, rune, 'zero or one arguments');
90+
} else if (rune === '$state' && node.arguments.length > 2) {
91+
e.rune_invalid_arguments_length(node, rune, 'zero, one or two arguments');
9292
}
9393

9494
break;

packages/svelte/src/compiler/phases/3-transform/client/transform-client.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,10 @@ export function client_component(analysis, options) {
292292
}
293293

294294
if (binding?.kind === 'state' || binding?.kind === 'raw_state') {
295-
const value = binding.kind === 'state' ? b.call('$.proxy', b.id('$$value')) : b.id('$$value');
295+
const value =
296+
binding.kind === 'state'
297+
? b.call('$.proxy', b.id('$$value'), b.call('$.get_options', b.id(name)))
298+
: b.id('$$value');
296299
return [getter, b.set(alias ?? name, [b.stmt(b.call('$.set', b.id(name), value))])];
297300
}
298301

packages/svelte/src/compiler/phases/3-transform/client/utils.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,9 @@ export function build_getter(node, state) {
5050
* @param {Expression} previous
5151
*/
5252
export function build_proxy_reassignment(value, previous) {
53-
return dev ? b.call('$.proxy', value, b.null, previous) : b.call('$.proxy', value);
53+
return dev
54+
? b.call('$.proxy', value, b.call('$.get_options', previous), b.null, previous)
55+
: b.call('$.proxy', value, b.call('$.get_options', previous));
5456
}
5557

5658
/**

packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassBody.js

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,11 +116,32 @@ export function ClassBody(node, context) {
116116
context.visit(definition.value.arguments[0], child_state)
117117
);
118118

119+
let options =
120+
definition.value.arguments.length === 2
121+
? /** @type {Expression} **/ (
122+
context.visit(definition.value.arguments[1], child_state)
123+
)
124+
: undefined;
125+
126+
let proxied = should_proxy(init, context.state.scope);
127+
128+
if (field.kind === 'state' && proxied && options != null) {
129+
let generated = 'state_options';
130+
let i = 0;
131+
while (private_ids.includes(generated)) {
132+
generated = `state_options_${i++}`;
133+
}
134+
private_ids.push(generated);
135+
body.push(b.prop_def(b.private_id(generated), options));
136+
options = b.member(b.this, `#${generated}`);
137+
}
138+
119139
value =
120140
field.kind === 'state'
121141
? b.call(
122142
'$.state',
123-
should_proxy(init, context.state.scope) ? b.call('$.proxy', init) : init
143+
should_proxy(init, context.state.scope) ? b.call('$.proxy', init, options) : init,
144+
options
124145
)
125146
: field.kind === 'raw_state'
126147
? b.call('$.state', init)

packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -113,28 +113,37 @@ export function VariableDeclaration(node, context) {
113113
const args = /** @type {CallExpression} */ (init).arguments;
114114
const value =
115115
args.length === 0 ? b.id('undefined') : /** @type {Expression} */ (context.visit(args[0]));
116+
let options =
117+
args.length === 2 ? /** @type {Expression} */ (context.visit(args[1])) : undefined;
116118

117119
if (rune === '$state' || rune === '$state.raw') {
118120
/**
119121
* @param {Identifier} id
120122
* @param {Expression} value
123+
* @param {Expression} [options]
121124
*/
122-
const create_state_declarator = (id, value) => {
125+
const create_state_declarator = (id, value, options) => {
123126
const binding = /** @type {import('#compiler').Binding} */ (
124127
context.state.scope.get(id.name)
125128
);
126-
if (rune === '$state' && should_proxy(value, context.state.scope)) {
127-
value = b.call('$.proxy', value);
129+
const proxied = rune === '$state' && should_proxy(value, context.state.scope);
130+
if (proxied) {
131+
if (options != null) {
132+
const generated = context.state.scope.generate('state_options');
133+
declarations.push(b.declarator(generated, options));
134+
options = b.id(generated);
135+
}
136+
value = b.call('$.proxy', value, options);
128137
}
129138
if (is_state_source(binding, context.state.analysis)) {
130-
value = b.call('$.state', value);
139+
value = b.call('$.state', value, options);
131140
}
132141
return value;
133142
};
134143

135144
if (declarator.id.type === 'Identifier') {
136145
declarations.push(
137-
b.declarator(declarator.id, create_state_declarator(declarator.id, value))
146+
b.declarator(declarator.id, create_state_declarator(declarator.id, value, options))
138147
);
139148
} else {
140149
const tmp = context.state.scope.generate('tmp');
@@ -147,7 +156,7 @@ export function VariableDeclaration(node, context) {
147156
return b.declarator(
148157
path.node,
149158
binding?.kind === 'state' || binding?.kind === 'raw_state'
150-
? create_state_declarator(binding.node, value)
159+
? create_state_declarator(binding.node, value, options)
151160
: value
152161
);
153162
})

packages/svelte/src/index.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,4 +351,6 @@ export type MountOptions<Props extends Record<string, any> = Record<string, any>
351351
props: Props;
352352
});
353353

354+
export { ValueOptions as StateOptions } from './internal/client/types.js';
355+
354356
export * from './index-client.js';

packages/svelte/src/internal/client/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ export {
109109
user_effect,
110110
user_pre_effect
111111
} from './reactivity/effects.js';
112-
export { mutable_state, mutate, set, state } from './reactivity/sources.js';
112+
export { mutable_state, mutate, set, state, get_options } from './reactivity/sources.js';
113113
export {
114114
prop,
115115
rest_props,

packages/svelte/src/internal/client/proxy.js

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/** @import { ProxyMetadata, ProxyStateObject, Source } from '#client' */
1+
/** @import { ProxyMetadata, ProxyStateObject, Source, ValueOptions } from '#client' */
22
import { DEV } from 'esm-env';
33
import { get, component_context, active_effect } from './runtime.js';
44
import {
@@ -19,11 +19,12 @@ import { tracing_mode_flag } from '../flags/index.js';
1919
/**
2020
* @template T
2121
* @param {T} value
22+
* @param {ValueOptions} [options]
2223
* @param {ProxyMetadata | null} [parent]
2324
* @param {Source<T>} [prev] dev mode only
2425
* @returns {T}
2526
*/
26-
export function proxy(value, parent = null, prev) {
27+
export function proxy(value, options, parent = null, prev) {
2728
/** @type {Error | null} */
2829
var stack = null;
2930
if (DEV && tracing_mode_flag) {
@@ -48,7 +49,7 @@ export function proxy(value, parent = null, prev) {
4849
if (is_proxied_array) {
4950
// We need to create the length source eagerly to ensure that
5051
// mutations to the array are properly synced with our proxy
51-
sources.set('length', source(/** @type {any[]} */ (value).length, stack));
52+
sources.set('length', source(/** @type {any[]} */ (value).length, options, stack));
5253
}
5354

5455
/** @type {ProxyMetadata} */
@@ -94,10 +95,10 @@ export function proxy(value, parent = null, prev) {
9495
var s = sources.get(prop);
9596

9697
if (s === undefined) {
97-
s = source(descriptor.value, stack);
98+
s = source(descriptor.value, options, stack);
9899
sources.set(prop, s);
99100
} else {
100-
set(s, proxy(descriptor.value, metadata));
101+
set(s, proxy(descriptor.value, options, metadata));
101102
}
102103

103104
return true;
@@ -108,7 +109,7 @@ export function proxy(value, parent = null, prev) {
108109

109110
if (s === undefined) {
110111
if (prop in target) {
111-
sources.set(prop, source(UNINITIALIZED, stack));
112+
sources.set(prop, source(UNINITIALIZED, options, stack));
112113
}
113114
} else {
114115
// When working with arrays, we need to also ensure we update the length when removing
@@ -142,7 +143,7 @@ export function proxy(value, parent = null, prev) {
142143

143144
// create a source, but only if it's an own property and not a prototype property
144145
if (s === undefined && (!exists || get_descriptor(target, prop)?.writable)) {
145-
s = source(proxy(exists ? target[prop] : UNINITIALIZED, metadata), stack);
146+
s = source(proxy(exists ? target[prop] : UNINITIALIZED, options, metadata), options, stack);
146147
sources.set(prop, s);
147148
}
148149

@@ -210,7 +211,7 @@ export function proxy(value, parent = null, prev) {
210211
(active_effect !== null && (!has || get_descriptor(target, prop)?.writable))
211212
) {
212213
if (s === undefined) {
213-
s = source(has ? proxy(target[prop], metadata) : UNINITIALIZED, stack);
214+
s = source(has ? proxy(target[prop], options, metadata) : UNINITIALIZED, options, stack);
214215
sources.set(prop, s);
215216
}
216217

@@ -237,7 +238,7 @@ export function proxy(value, parent = null, prev) {
237238
// If the item exists in the original, we need to create a uninitialized source,
238239
// else a later read of the property would result in a source being created with
239240
// the value of the original item at that index.
240-
other_s = source(UNINITIALIZED, stack);
241+
other_s = source(UNINITIALIZED, options, stack);
241242
sources.set(i + '', other_s);
242243
}
243244
}
@@ -249,13 +250,13 @@ export function proxy(value, parent = null, prev) {
249250
// object property before writing to that property.
250251
if (s === undefined) {
251252
if (!has || get_descriptor(target, prop)?.writable) {
252-
s = source(undefined, stack);
253-
set(s, proxy(value, metadata));
253+
s = source(undefined, options, stack);
254+
set(s, proxy(value, options, metadata));
254255
sources.set(prop, s);
255256
}
256257
} else {
257258
has = s.v !== UNINITIALIZED;
258-
set(s, proxy(value, metadata));
259+
set(s, proxy(value, options, metadata));
259260
}
260261

261262
if (DEV) {

0 commit comments

Comments
 (0)