From de23027259eb6d564309773552cc0f90535cf910 Mon Sep 17 00:00:00 2001 From: Dmitry Boldyrev Date: Fri, 15 Dec 2023 16:40:28 +0600 Subject: [PATCH] Add `spread(targets)` shorthand --- src/spread/index.ts | 57 ++++++++++--- src/spread/readme.md | 175 +++++++++++++++++++++----------------- src/spread/spread.test.ts | 72 +++++++++++++--- test-typings/spread.ts | 99 +++++++++++++++++++++ 4 files changed, 302 insertions(+), 101 deletions(-) diff --git a/src/spread/index.ts b/src/spread/index.ts index 722a596f..db7f1130 100644 --- a/src/spread/index.ts +++ b/src/spread/index.ts @@ -1,11 +1,17 @@ -import { createEvent, Event, EventCallable, sample, Unit, UnitTargetable } from 'effector'; +import { + createEvent, + EventCallable, + is, + sample, + Unit, + UnitTargetable, +} from 'effector'; const hasPropBase = {}.hasOwnProperty; const hasOwnProp = (object: O, key: string) => hasPropBase.call(object, key); type NoInfer = [T][T extends any ? 0 : never]; -type EventAsReturnType = any extends Payload ? Event : never; export function spread(config: { targets: { @@ -25,6 +31,10 @@ export function spread< }; }): Source; +export function spread(targets: { + [Key in keyof Payload]?: UnitTargetable; +}): EventCallable>; + /** * @example * spread({ source: dataObject, targets: { first: targetA, second: targetB } }) @@ -32,15 +42,20 @@ export function spread< * target: spread({targets: { first: targetA, second: targetB } }) * }) */ -export function spread

({ - targets, - source = createEvent

(), -}: { - targets: { - [Key in keyof P]?: Unit; - }; - source?: Unit

; -}): EventCallable

{ +export function spread

( + args: + | { + targets: { + [Key in keyof P]?: Unit; + }; + source?: Unit

; + } + | { + [Key in keyof P]?: Unit; + }, +): EventCallable

{ + const argsShape = isTargets(args) ? { targets: args } : args; + const { targets, source = createEvent

() } = argsShape; for (const targetKey in targets) { if (hasOwnProp(targets, targetKey)) { const currentTarget = targets[targetKey]; @@ -63,3 +78,23 @@ export function spread

({ return source as any; } + +function isTargets

( + args: + | { + targets: { + [Key in keyof P]?: Unit; + }; + source?: Unit

; + } + | { + [Key in keyof P]?: Unit; + }, +): args is { + [Key in keyof P]?: Unit; +} { + return Object.keys(args).some( + (key) => + !['targets', 'source'].includes(key) && is.unit(args[key as keyof typeof args]), + ); +} diff --git a/src/spread/readme.md b/src/spread/readme.md index b4d96020..941e8753 100644 --- a/src/spread/readme.md +++ b/src/spread/readme.md @@ -6,6 +6,101 @@ import { spread } from 'patronum'; import { spread } from 'patronum/spread'; ``` +## `source = spread(targets)` + +:::note since +patronum 2.1.0 +Use `spread({ targets })` with patronum < 2.1.0 +::: + +### Motivation + +This method allows to trigger many target at once, if they match the source structure. +It is useful when you need to destructure object and save values to different stores. + +### Formulae + +```ts +source = spread({ field: target, ... }) +``` + +- When `source` is triggered with **object**, extract `field` from data, and trigger `target` +- `targets` can have multiple properties +- If the `source` was triggered with non-object, nothing would be happening +- If `source` is triggered with object but without propertpy `field`, target for this `field` will not be triggered + +### Arguments + +1. `targets` `(Record | Store | Effect>)` — Flat object which key is key in `source` payload, and value is unit to store value to. + +### Returns + +- `source` `(Event)` — Source event, data passed to it should be an object with fields from `targets` + +### Example + +#### Conditionally save value to stores + +```ts +import { createStore, createEvent, sample } from 'effector'; +import { spread } from 'patronum'; + +const $first = createStore(''); +const $last = createStore(''); + +const formReceived = createEvent(); + +sample({ + source: formReceived, + filter: (form) => form.first.length > 0 && form.last.length > 0, + target: spread({ + first: $first, + last: $last, + }), +}); + +$first.watch((first) => console.log('First name', first)); +$last.watch((last) => console.log('Last name', last)); + +formReceived({ first: '', last: '' }); +// Nothing, because filter returned true + +formReceived({ first: 'Hello', last: 'World' }); +// => First name Hello +// => Last name World +``` + +#### Nested spreading + +```ts +const $targetA = createStore(''); +const $targetB = createStore(0); +const $targetC = createStore(false); + +const trigger = spread({ + first: $targetA, + second: spread({ + foo: $targetB, + bar: $targetC, + }), +}); + +$targetA.watch((payload) => console.log('targetA', payload)); +$targetB.watch((payload) => console.log('targetB', payload)); +$targetC.watch((payload) => console.log('targetC', payload)); + +trigger({ + first: 'Hello', + second: { + foo: 200, + bar: true, + }, +}); +// => targetA Hello +// => targetB 200 +// => targetC true +``` + ## `spread({ source, targets })` ### Motivation @@ -35,7 +130,7 @@ spread({ source, targets: { field: target, ... } }) ```ts import { createStore, createEvent } from 'effector'; -import { spread } from 'patronum/spread'; +import { spread } from 'patronum'; const $first = createStore(''); const $last = createStore(''); @@ -96,8 +191,7 @@ save(null); ### Motivation -This overload creates event `source` that should be triggered and returns it. -It is useful to pass `source` immediately to another method as argument. +This overload recieves `targets` as an object. May be useful for additional clarity, but it's longer to write ### Formulae @@ -116,77 +210,4 @@ source = spread({ targets: { field: target, ... } }) ### Returns -- `source` `(Event` | `Store` | `Effect)` — Source unit, data passed to it should be an object with fields from `targets` - -### Example - -#### Conditionally save value to stores - -```ts -import { createStore, createEvent, sample } from 'effector'; -import { spread } from 'patronum/spread'; - -const $first = createStore(''); -const $last = createStore(''); - -const formReceived = createEvent(); - -sample({ - source: formReceived, - filter: (form) => form.first.length > 0 && form.last.length > 0, - target: spread({ - targets: { - first: $first, - last: $last, - }, - }), -}); - -$first.watch((first) => console.log('First name', first)); -$last.watch((last) => console.log('Last name', last)); - -formReceived({ first: '', last: '' }); -// Nothing, because filter returned true - -formReceived({ first: 'Hello', last: 'World' }); -// => First name Hello -// => Last name World -``` - -#### Nested spreading - -```ts -const trigger = createEvent(); - -const $targetA = createStore(''); -const $targetB = createStore(0); -const $targetC = createStore(false); - -spread({ - source: trigger, - targets: { - first: $targetA, - second: spread({ - targets: { - foo: $targetB, - bar: $targetC, - }, - }), - }, -}); - -$targetA.watch((payload) => console.log('targetA', payload)); -$targetB.watch((payload) => console.log('targetB', payload)); -$targetC.watch((payload) => console.log('targetC', payload)); - -trigger({ - first: 'Hello', - second: { - foo: 200, - bar: true, - }, -}); -// => targetA Hello -// => targetB 200 -// => targetC true -``` +- `source` `(Event)` — Source event, data passed to it should be an object with fields from `targets` diff --git a/src/spread/spread.test.ts b/src/spread/spread.test.ts index a62a3a1e..b3fef538 100644 --- a/src/spread/spread.test.ts +++ b/src/spread/spread.test.ts @@ -133,23 +133,46 @@ describe('spread(targets)', () => { expect(fnA).toBeCalledWith('Hello'); expect(fnB).toBeCalledWith(200); }); - - test('event to stores', () => { + test('event to events (shorthand)', () => { const source = createEvent<{ first: string; second: number }>(); - const targetA = createStore(''); - const targetB = createStore(0); + const targetA = createEvent(); + const targetB = createEvent(); const fnA = jest.fn(); const fnB = jest.fn(); targetA.watch(fnA); targetB.watch(fnB); + sample({ + clock: source, + target: spread({ + first: targetA, + second: targetB, + }), + }); + + source({ first: 'Hello', second: 200 }); + + expect(fnA).toBeCalledWith('Hello'); + expect(fnB).toBeCalledWith(200); + }); + + test('event to stores', () => { + const source = createEvent<{ first: string; second: number }>(); + const $targetA = createStore(''); + const $targetB = createStore(0); + + const fnA = jest.fn(); + const fnB = jest.fn(); + $targetA.watch(fnA); + $targetB.watch(fnB); + sample({ clock: source, target: spread({ targets: { - first: targetA, - second: targetB, + first: $targetA, + second: $targetB, }, }), }); @@ -160,26 +183,50 @@ describe('spread(targets)', () => { expect(fnB).toBeCalledWith(200); }); + test('event to stores (shorthand)', () => { + const source = createEvent<{ first: string; second: number }>(); + const $targetA = createStore(''); + const $targetB = createStore(0); + + const fnA = jest.fn(); + const fnB = jest.fn(); + $targetA.watch(fnA); + $targetB.watch(fnB); + + sample({ + clock: source, + target: spread({ + first: $targetA, + second: $targetB, + }), + }); + + source({ first: 'Hello', second: 200 }); + + expect(fnA).toBeCalledWith('Hello'); + expect(fnB).toBeCalledWith(200); + }); + test('store to stores', () => { const change = createEvent<{ first: string; second: number }>(); const source = createStore({ first: 'hello', second: 200 }).on( change, (_, value) => value, ); - const targetA = createStore(''); - const targetB = createStore(0); + const $targetA = createStore(''); + const $targetB = createStore(0); const fnA = jest.fn(); const fnB = jest.fn(); - targetA.watch(fnA); - targetB.watch(fnB); + $targetA.watch(fnA); + $targetB.watch(fnB); sample({ clock: source, target: spread({ targets: { - first: targetA, - second: targetB, + first: $targetA, + second: $targetB, }, }), }); @@ -427,7 +474,6 @@ describe('invalid', () => { }, }); - // @ts-expect-error no argument source(); source(1); source(''); diff --git a/test-typings/spread.ts b/test-typings/spread.ts index 617d8fa2..eafd304d 100644 --- a/test-typings/spread.ts +++ b/test-typings/spread.ts @@ -149,6 +149,15 @@ import { spread } from '../dist/spread'; expectType>(spreadToStores); } +{ + const spreadToStores = spread({ + foo: createStore(''), + bar: createEffect(), + baz: createEvent(), + }); + + expectType>(spreadToStores); +} // Example from readme with nullability { @@ -307,3 +316,93 @@ import { spread } from '../dist/spread'; }), }); } +{ + const $source = createStore({ first: '', last: { nested: '', other: '' } }); + const first = createEvent(); + const nested = createEvent(); + const other = createEvent(); + + // nested full match + spread({ + source: $source, + targets: { + first, + last: spread({ + nested, + other, + }), + }, + }); + + // nested partial match + spread({ + source: $source, + targets: { + first, + last: spread({ nested }), + }, + }); + + // nested wrong match + // @ts-expect-error + spread({ + source: $source, + targets: { + first, + last: other, + }, + }); + + // nested full match outer + const out = spread({ + nested, + other, + }); + + spread({ + source: $source, + targets: { + first, + last: out, + }, + }); + + // nested partial match outer + const outPart = spread({ nested }); + + spread({ + source: $source, + targets: { + first, + last: outPart, + }, + }); + + // sample partial match + sample({ + clock: $source, + target: spread({ first }), + }); + + // sample full match + sample({ + clock: $source, + target: spread({ + first, + last: spread({ + nested, + other, + }), + }), + }); + + // sample wrong match + sample({ + // @ts-expect-error + clock: $source, + target: spread({ + first, + last: other, + }), + }); +}