From 10ab317c8c9d680cd4df4d016df0ba8f8bc5fde0 Mon Sep 17 00:00:00 2001 From: Robin Munn Date: Tue, 21 Sep 2021 10:54:15 +0700 Subject: [PATCH 1/5] Pass update function to store setup callbacks This non-breaking change allows more complex store logic to be implemented, such as a derived store that accumulates a history of its parent store's values. --- src/runtime/store/index.ts | 10 ++--- test/store/index.ts | 77 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 5 deletions(-) diff --git a/src/runtime/store/index.ts b/src/runtime/store/index.ts index e947fa074028..afd4ccabd61e 100644 --- a/src/runtime/store/index.ts +++ b/src/runtime/store/index.ts @@ -13,7 +13,7 @@ export type Updater = (value: T) => T; type Invalidator = (value?: T) => void; /** Start and stop notification callbacks. */ -export type StartStopNotifier = (set: Subscriber) => Unsubscriber | void; +export type StartStopNotifier = (set: Subscriber, update?: (fn: Updater) => void) => Unsubscriber | void; /** Readable interface for subscribing. */ export interface Readable { @@ -92,7 +92,7 @@ export function writable(value?: T, start: StartStopNotifier = noop): Writ const subscriber: SubscribeInvalidateTuple = [run, invalidate]; subscribers.add(subscriber); if (subscribers.size === 1) { - stop = start(set) || noop; + stop = start(set, update) || noop; } run(value); @@ -125,7 +125,7 @@ type StoresValues = T extends Readable ? U : */ export function derived( stores: S, - fn: (values: StoresValues, set: (value: T) => void) => Unsubscriber | void, + fn: (values: StoresValues, set: Subscriber, update?: (fn: Updater) => void) => Unsubscriber | void, initial_value?: T ): Readable; @@ -163,7 +163,7 @@ export function derived(stores: Stores, fn: Function, initial_value?: T): Rea const auto = fn.length < 2; - return readable(initial_value, (set) => { + return readable(initial_value, (set, update) => { let inited = false; const values = []; @@ -175,7 +175,7 @@ export function derived(stores: Stores, fn: Function, initial_value?: T): Rea return; } cleanup(); - const result = fn(single ? values[0] : values, set); + const result = fn(single ? values[0] : values, set, update); if (auto) { set(result as T); } else { diff --git a/test/store/index.ts b/test/store/index.ts index b6fc5940e111..e49bf0572e08 100644 --- a/test/store/index.ts +++ b/test/store/index.ts @@ -128,6 +128,50 @@ describe('store', () => { assert.deepEqual(values, [0, 1, 2]); }); + it('passes an optional update function', () => { + let running; + let tick; + let add; + + const store = readable(undefined, (set, update) => { + tick = set; + running = true; + add = n => update(value => value + n); + + set(0); + + return () => { + tick = () => { }; + add = _ => { }; + running = false; + }; + }); + + assert.ok(!running); + + const values = []; + + const unsubscribe = store.subscribe(value => { + values.push(value); + }); + + assert.ok(running); + tick(1); + tick(2); + add(3); + add(4); + tick(5); + add(6); + + unsubscribe(); + + assert.ok(!running); + tick(7); + add(8); + + assert.deepEqual(values, [0, 1, 2, 5, 9, 5, 11]); + }); + it('creates an undefined readable store', () => { const store = readable(); const values = []; @@ -231,6 +275,39 @@ describe('store', () => { assert.deepEqual(values, [0, 2, 4]); }); + it('passes optional set and update functions', () => { + const number = writable(1); + const evensAndSquaresOf4 = derived(number, (n, set, update) => { + if (n % 2 === 0) set(n); + if (n % 4 === 0) update(n => n * n); + }, 0); + + const values = []; + + const unsubscribe = evensAndSquaresOf4.subscribe(value => { + values.push(value); + }); + + number.set(2); + number.set(3); + number.set(4); + number.set(5); + number.set(6); + assert.deepEqual(values, [0, 2, 4, 16, 6]); + + number.set(7); + number.set(8); + number.set(9); + number.set(10); + assert.deepEqual(values, [0, 2, 4, 16, 6, 8, 64, 10]); + + unsubscribe(); + + number.set(11); + number.set(12); + assert.deepEqual(values, [0, 2, 4, 16, 6, 8, 64, 10]); + }); + it('prevents glitches', () => { const lastname = writable('Jekyll'); const firstname = derived(lastname, n => n === 'Jekyll' ? 'Henry' : 'Edward'); From b2b7bd1c8d1c68caeb8045d69dabbdd4feea445d Mon Sep 17 00:00:00 2001 From: Robin Munn Date: Tue, 21 Sep 2021 13:25:01 +0700 Subject: [PATCH 2/5] Document new update param to store callbacks --- site/content/docs/03-run-time.md | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/site/content/docs/03-run-time.md b/site/content/docs/03-run-time.md index 2b49a9399fde..8515297c3285 100644 --- a/site/content/docs/03-run-time.md +++ b/site/content/docs/03-run-time.md @@ -268,7 +268,7 @@ This makes it possible to wrap almost any other reactive state handling library store = writable(value?: any) ``` ```js -store = writable(value?: any, start?: (set: (value: any) => void) => () => void) +store = writable(value?: any, start?: (set: (value: any) => void, update?: (fn: any => any) => void) => () => void) ``` --- @@ -295,7 +295,7 @@ count.update(n => n + 1); // logs '2' --- -If a function is passed as the second argument, it will be called when the number of subscribers goes from zero to one (but not from one to two, etc). That function will be passed a `set` function which changes the value of the store. It must return a `stop` function that is called when the subscriber count goes from one to zero. +If a function is passed as the second argument, it will be called when the number of subscribers goes from zero to one (but not from one to two, etc). That function will be passed a `set` function which changes the value of the store, and optionally an `update` function which works like the `update` method on the store, taking a callback to calculate the store's new value from its old value. It must return a `stop` function that is called when the subscriber count goes from one to zero. ```js import { writable } from 'svelte/store'; @@ -338,6 +338,16 @@ const time = readable(null, set => { return () => clearInterval(interval); }); + +const ticktock = readable(null, (set, update) => { + set('tick'); + + const interval = setInterval(() => { + update(sound => sound === 'tick' ? 'tock' : 'tick'); + }, 1000); + + return () => clearInterval(interval); +}); ``` #### `derived` @@ -346,13 +356,13 @@ const time = readable(null, set => { store = derived(a, callback: (a: any) => any) ``` ```js -store = derived(a, callback: (a: any, set: (value: any) => void) => void | () => void, initial_value: any) +store = derived(a, callback: (a: any, set: (value: any) => void, update?: (fn: any => any) => void) => void | () => void, initial_value: any) ``` ```js store = derived([a, ...b], callback: ([a: any, ...b: any[]]) => any) ``` ```js -store = derived([a, ...b], callback: ([a: any, ...b: any[]], set: (value: any) => void) => void | () => void, initial_value: any) +store = derived([a, ...b], callback: ([a: any, ...b: any[]], set: (value: any) => void, update?: (fn: any => any) => void) => void | () => void, initial_value: any) ``` --- @@ -369,9 +379,9 @@ const doubled = derived(a, $a => $a * 2); --- -The callback can set a value asynchronously by accepting a second argument, `set`, and calling it when appropriate. +The callback can set a value asynchronously by accepting a second argument, `set`, and an optional third argument, `update`, calling either or both of them when appropriate. -In this case, you can also pass a third argument to `derived` — the initial value of the derived store before `set` is first called. +In this case, you can also pass a third argument to `derived` — the initial value of the derived store before `set` or `update` is first called. If no initial value is specified, the store's initial value will be `undefined`. ```js import { derived } from 'svelte/store'; @@ -379,6 +389,13 @@ import { derived } from 'svelte/store'; const delayed = derived(a, ($a, set) => { setTimeout(() => set($a), 1000); }, 'one moment...'); + +const delayedIncrement = derived(a, ($a, set, update) => { + set($a); + setTimeout(() => update(x => x + 1), 1000); + // every time $a produces a value, this produces two + // values, $a immediately and then $a + 1 a second later +}); ``` --- @@ -808,7 +825,7 @@ The `crossfade` function creates a pair of [transitions](docs#transition_fn) cal * `delay` (`number`, default 0) — milliseconds before starting * `duration` (`number` | `function`, default 800) — milliseconds the transition lasts * `easing` (`function`, default `cubicOut`) — an [easing function](docs#svelte_easing) -* `fallback` (`function`) — A fallback [transition](docs#transition_fn) to use for send when there is no matching element being received, and for receive when there is no element being sent. +* `fallback` (`function`) — A fallback [transition](docs#transition_fn) to use for send when there is no matching element being received, and for receive when there is no element being sent. ```sv