Skip to content

Commit

Permalink
feat: spread - support array of units in target fields
Browse files Browse the repository at this point in the history
  • Loading branch information
earthspacon committed Jul 25, 2024
1 parent aa7b1eb commit cf60b04
Show file tree
Hide file tree
Showing 3 changed files with 210 additions and 28 deletions.
61 changes: 34 additions & 27 deletions src/spread/index.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,34 @@
import {
createEvent,
EventCallable,
is,
sample,
Unit,
UnitTargetable,
} from 'effector';
import { createEvent, EventCallable, sample, Unit, UnitTargetable } from 'effector';

const hasPropBase = {}.hasOwnProperty;
const hasOwnProp = <O extends { [k: string]: unknown }>(object: O, key: string) =>
hasPropBase.call(object, key);

type NoInfer<T> = [T][T extends any ? 0 : never];

/**
* @example
* spread({
* source: dataObject,
* targets: { first: targetA, second: [target1, target2] },
* })
*
* sample({
* source: dataObject,
* target: spread({ targets: { first: targetA, second: [target1, target2] } })
* })
*
* sample({
* source: dataObject,
* target: spread({ first: targetA, second: [target1, target2] })
* })
*/

export function spread<Payload>(config: {
targets: {
[Key in keyof Payload]?: UnitTargetable<Payload[Key]>;
[Key in keyof Payload]?:
| UnitTargetable<Payload[Key]>
| Array<UnitTargetable<Payload[Key]>>;
};
}): EventCallable<Partial<Payload>>;

Expand All @@ -27,31 +40,28 @@ export function spread<
targets: {
[Key in keyof Payload]?:
| EventCallable<Partial<Payload[Key]>>
| UnitTargetable<NoInfer<Payload[Key]>>;
| Array<EventCallable<Partial<Payload[Key]>>>
| UnitTargetable<NoInfer<Payload[Key]>>
| Array<UnitTargetable<NoInfer<Payload[Key]>>>;
};
}): Source;

export function spread<Payload>(targets: {
[Key in keyof Payload]?: UnitTargetable<Payload[Key]>;
[Key in keyof Payload]?:
| UnitTargetable<Payload[Key]>
| Array<UnitTargetable<Payload[Key]>>;
}): EventCallable<Partial<Payload>>;

/**
* @example
* spread({ source: dataObject, targets: { first: targetA, second: targetB } })
* sample({
* target: spread({targets: { first: targetA, second: targetB } })
* })
*/
export function spread<P>(
args:
| {
targets: {
[Key in keyof P]?: Unit<P[Key]>;
[Key in keyof P]?: Unit<P[Key]> | Array<Unit<P[Key]>>;
};
source?: Unit<P>;
}
| {
[Key in keyof P]?: Unit<P[Key]>;
[Key in keyof P]?: Unit<P[Key]> | Array<Unit<P[Key]>>;
},
): EventCallable<P> {
const argsShape = isTargets(args) ? { targets: args } : args;
Expand Down Expand Up @@ -83,18 +93,15 @@ function isTargets<P>(
args:
| {
targets: {
[Key in keyof P]?: Unit<P[Key]>;
[Key in keyof P]?: Unit<P[Key]> | Array<Unit<P[Key]>>;
};
source?: Unit<P>;
}
| {
[Key in keyof P]?: Unit<P[Key]>;
[Key in keyof P]?: Unit<P[Key]> | Array<Unit<P[Key]>>;
},
): args is {
[Key in keyof P]?: Unit<P[Key]>;
[Key in keyof P]?: Unit<P[Key]> | Array<Unit<P[Key]>>;
} {
return Object.keys(args).some(
(key) =>
!['targets', 'source'].includes(key) && is.unit(args[key as keyof typeof args]),
);
return !Object.keys(args).some((key) => ['targets', 'source'].includes(key));
}
58 changes: 58 additions & 0 deletions src/spread/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,3 +211,61 @@ source = spread({ targets: { field: target, ... } })
### Returns

- `source` `(Event<T>)` — Source event, data passed to it should be an object with fields from `targets`

## `source = spread({ targets: { field: Unit[] } })`

### Motivation

Multiple units can be passed for each target field

### Formulae

```ts
source = spread({ field: [target1, target2], ... })

source = spread({ targets: { field: [target1, target2], ... } })

spread({ source, targets: { field: [target1, target2], ... } })
```

- When `source` is triggered with **object**, extract `field` from data, and trigger all targets of `target`
- `targets` can have multiple properties with multiple units
- If the `source` was triggered with non-object, nothing would be happening
- If `source` is triggered with object but without property `field`, no unit of the target for this `field` will be triggered

### Example

#### Trigger multiple units for each field of payload

```ts
const roomEntered = createEvent<{
roomId: string;
userId: string;
message: string;
}>();
const userIdChanged = createEvent<string>();

const $roomMessage = createStore('');
const $currentRoomId = createStore<string | null>(null);

const getRoomFx = createEffect((roomId: string) => roomId);
const setUserIdFx = createEffect((userId: string) => userId);

sample({
clock: roomEntered,
target: spread({
roomId: [getRoomFx, $currentRoomId],
userId: [setUserIdFx, userIdChanged],
message: $roomMessage,
}),
});

roomEntered({
roomId: 'roomId',
userId: 'userId',
message: 'message',
});
// => getRoomFx('roomId'), update $currentRoomId with 'roomId'
// => setUserIdFx('userId'), userIdChanged('userId')
// => update $roomMessage with 'message'
```
119 changes: 118 additions & 1 deletion src/spread/spread.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { combine, createEvent, createStore, sample } from 'effector';
import { combine, createEffect, createEvent, createStore, sample } from 'effector';
import { spread } from './index';

describe('spread(source, targets)', () => {
Expand Down Expand Up @@ -105,6 +105,44 @@ describe('spread(source, targets)', () => {
expect(fnA).toBeCalledWith('Hello');
expect(fnB).toBeCalledWith(200);
});

test('unit to array of units', () => {
const source = createEvent<{ first: string; second: number; third: string }>();
const targetA = createEvent<string>();
const targetA2 = createEffect<string, void>();
const targetB = createEvent<number>();
const targetB2 = createEvent<number>();
const targetC = createStore('');

const fnA = jest.fn();
const fnB = jest.fn();
const fnA1 = jest.fn();
const fnB1 = jest.fn();
const fnC = jest.fn();

targetA.watch(fnA);
targetB.watch(fnB);
targetA2.watch(fnA1);
targetB2.watch(fnB1);
targetC.watch(fnC);

spread({
source,
targets: {
first: [targetA, targetA2],
second: [targetB, targetB2],
third: targetC,
},
});

source({ first: 'Hello', second: 200, third: 'third' });

expect(fnA).toBeCalledWith('Hello');
expect(fnB).toBeCalledWith(200);
expect(fnA1).toBeCalledWith('Hello');
expect(fnB1).toBeCalledWith(200);
expect(fnC).toBeCalledWith('third');
});
});

describe('spread(targets)', () => {
Expand Down Expand Up @@ -133,6 +171,7 @@ describe('spread(targets)', () => {
expect(fnA).toBeCalledWith('Hello');
expect(fnB).toBeCalledWith(200);
});

test('event to events (shorthand)', () => {
const source = createEvent<{ first: string; second: number }>();
const targetA = createEvent<string>();
Expand Down Expand Up @@ -266,6 +305,84 @@ describe('spread(targets)', () => {
expect(fnA).toBeCalledWith('Hello');
expect(fnB).toBeCalledWith(200);
});

test('unit to array of units', () => {
const source = createEvent<{ first: string; second: number; third: string }>();
const targetA = createEvent<string>();
const targetA2 = createEffect<string, void>();
const targetB = createEvent<number>();
const targetB2 = createEvent<number>();
const targetC = createStore('');

const fnA = jest.fn();
const fnB = jest.fn();
const fnA1 = jest.fn();
const fnB1 = jest.fn();
const fnC = jest.fn();

targetA.watch(fnA);
targetB.watch(fnB);
targetA2.watch(fnA1);
targetB2.watch(fnB1);
targetC.watch(fnC);

sample({
source,
target: spread({
targets: {
first: [targetA, targetA2],
second: [targetB, targetB2],
third: targetC,
},
}),
});

source({ first: 'Hello', second: 200, third: 'third' });

expect(fnA).toBeCalledWith('Hello');
expect(fnB).toBeCalledWith(200);
expect(fnA1).toBeCalledWith('Hello');
expect(fnB1).toBeCalledWith(200);
expect(fnC).toBeCalledWith('third');
});

test('unit to array of units (shorthand)', () => {
const source = createEvent<{ first: string; second: number; third: string }>();
const targetA = createEvent<string>();
const targetA2 = createEffect<string, void>();
const targetB = createEvent<number>();
const targetB2 = createEvent<number>();
const targetC = createStore('');

const fnA = jest.fn();
const fnB = jest.fn();
const fnA1 = jest.fn();
const fnB1 = jest.fn();
const fnC = jest.fn();

targetA.watch(fnA);
targetB.watch(fnB);
targetA2.watch(fnA1);
targetB2.watch(fnB1);
targetC.watch(fnC);

sample({
source,
target: spread({
first: [targetA, targetA2],
second: [targetB, targetB2],
third: targetC,
}),
});

source({ first: 'Hello', second: 200, third: 'third' });

expect(fnA).toBeCalledWith('Hello');
expect(fnB).toBeCalledWith(200);
expect(fnA1).toBeCalledWith('Hello');
expect(fnB1).toBeCalledWith(200);
expect(fnC).toBeCalledWith('third');
});
});

describe('edge', () => {
Expand Down

0 comments on commit cf60b04

Please sign in to comment.