Skip to content

Commit 3c2c7d9

Browse files
committed
Add always() and cond()
1 parent bcb0feb commit 3c2c7d9

2 files changed

Lines changed: 143 additions & 13 deletions

File tree

src/index.test.ts

Lines changed: 98 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { call, entry, exit, on, start } from "./index";
1+
import { always, call, cond, entry, exit, on, start } from "./index";
22

33
const fetch = jest.fn();
44
beforeEach(fetch.mockClear);
@@ -7,6 +7,11 @@ const finishedLoading = jest.fn();
77
beforeEach(finishedLoading.mockClear);
88

99
describe("Machine with entry and exit actions", () => {
10+
const someURL = new URL("https://example.org/");
11+
function fetchData() {
12+
return fetch(someURL.toString());
13+
}
14+
1015
function Loader() {
1116
function* idle() {
1217
yield on("FETCH", loading);
@@ -25,11 +30,6 @@ describe("Machine with entry and exit actions", () => {
2530
return idle;
2631
}
2732

28-
const someURL = new URL("https://example.org/");
29-
function fetchData() {
30-
return fetch(someURL.toString());
31-
}
32-
3333
test("creating", () => {
3434
const loader = start(Loader);
3535
expect(loader).toBeDefined();
@@ -85,12 +85,16 @@ describe("Machine with entry and exit actions", () => {
8585
const transitionResult = loader.next("FETCH");
8686
expect(fetch).toHaveBeenCalledTimes(1);
8787
expect(fetch).toHaveBeenLastCalledWith("https://example.org/");
88-
expect(transitionResult.actions).toEqual([{ type: "entry", f: fetchData }]);
88+
expect(transitionResult.actions).toEqual([
89+
{ type: "entry", f: fetchData },
90+
]);
8991
expect(loader.value).toEqual("loading");
9092
expect(loader.changeCount).toEqual(1);
9193

9294
await expect(loader.resolved).rejects.toEqual(new Error("Failed!"));
93-
await expect(Promise.resolve(transitionResult)).rejects.toEqual(new Error("Failed!"));
95+
await expect(Promise.resolve(transitionResult)).rejects.toEqual(
96+
new Error("Failed!")
97+
);
9498
expect(loader.changeCount).toEqual(2);
9599
expect(loader.value).toEqual("failure");
96100

@@ -112,6 +116,92 @@ describe("Machine with entry and exit actions", () => {
112116
});
113117
});
114118

119+
describe("Form Field Machine with entry and exit actions", () => {
120+
// const validate = jest.fn();
121+
// beforeEach(validate.mockClear);
122+
const isValid = jest.fn();
123+
beforeEach(isValid.mockClear);
124+
125+
function FormField() {
126+
function* initial() {
127+
yield on("CHANGE", editing);
128+
}
129+
function* editing() {
130+
// yield exit(validate);
131+
yield on("CHANGE", editing);
132+
yield on("BLUR", validating);
133+
}
134+
function* validating() {
135+
yield always(cond(isValid, valid));
136+
yield always(invalid);
137+
138+
// yield on(null, cond(isValid, valid));
139+
// yield on(null, invalid);
140+
141+
// yield always([cond(isValid, valid), invalid]);
142+
// return [cond(isValid, valid), invalid];
143+
// return conds([[isValid, valid], [true, invalid]]);
144+
}
145+
function* invalid() {}
146+
function* valid() {}
147+
148+
return initial;
149+
}
150+
151+
test("creating", () => {
152+
const formField = start(FormField);
153+
expect(formField).toBeDefined();
154+
});
155+
156+
describe("when is valid", () => {
157+
beforeEach(() => {
158+
isValid.mockReturnValue(true);
159+
});
160+
161+
test("sending events", () => {
162+
const formField = start(FormField);
163+
expect(formField).toBeDefined();
164+
expect(formField.value).toEqual("initial");
165+
166+
formField.next("CHANGE");
167+
expect(formField.value).toEqual("editing");
168+
expect(formField.changeCount).toEqual(1);
169+
170+
formField.next("CHANGE");
171+
expect(formField.value).toEqual("editing");
172+
expect(formField.changeCount).toEqual(2);
173+
174+
formField.next("BLUR");
175+
expect(formField.value).toEqual("valid");
176+
expect(formField.changeCount).toEqual(4);
177+
});
178+
});
179+
180+
describe("when is invalid", () => {
181+
beforeEach(() => {
182+
isValid.mockReturnValue(false);
183+
});
184+
185+
test("sending events", () => {
186+
const formField = start(FormField);
187+
expect(formField).toBeDefined();
188+
expect(formField.value).toEqual("initial");
189+
190+
formField.next("CHANGE");
191+
expect(formField.value).toEqual("editing");
192+
expect(formField.changeCount).toEqual(1);
193+
194+
formField.next("CHANGE");
195+
expect(formField.value).toEqual("editing");
196+
expect(formField.changeCount).toEqual(2);
197+
198+
formField.next("BLUR");
199+
expect(formField.value).toEqual("invalid");
200+
expect(formField.changeCount).toEqual(4);
201+
});
202+
});
203+
});
204+
115205
describe("Machine with call", () => {
116206
function Loader({ url }: { url: URL }) {
117207
function* idle() {

src/index.ts

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,24 @@ export interface ExitAction {
1717
f: ActionBody;
1818
}
1919

20+
export type StateDefinition = () => Generator<Yielded, any, unknown>;
21+
export interface Cond {
22+
type: "cond";
23+
cond: Function;
24+
target: StateDefinition;
25+
}
26+
export type Target = StateDefinition | Cond;
2027
export interface On {
2128
type: "on";
2229
on: string;
23-
target: Function;
30+
target: Target;
31+
}
32+
export interface Always {
33+
type: "always";
34+
target: Target;
2435
}
2536

26-
export type Yielded = On | EntryAction | ExitAction | Call<any>;
37+
export type Yielded = On | Always | EntryAction | ExitAction | Call<any>;
2738

2839
export function call<Arguments extends Array<any>>(
2940
f: (...args: Arguments) => void,
@@ -40,10 +51,18 @@ export function exit(f: ActionBody): ExitAction {
4051
return { type: "exit", f };
4152
}
4253

43-
export function on<Event extends string>(event: Event, target: Function): On {
54+
export function on<Event extends string>(event: Event, target: Target): On {
4455
return { type: "on", on: event, target };
4556
}
4657

58+
export function always(target: Target): Always {
59+
return { type: "always", target };
60+
}
61+
62+
export function cond(cond: () => boolean, target: StateDefinition): Cond {
63+
return { type: "cond", cond, target };
64+
}
65+
4766
export interface MachineInstance extends Iterator<string, void, string> {
4867
changeCount: number;
4968
value: string;
@@ -63,7 +82,8 @@ export function start<Arguments extends Array<any>>(
6382
): MachineInstance {
6483
const initialGenerator = machine.apply(null, args);
6584

66-
const eventsMap = new Map();
85+
const eventsMap: Map<string, Target> = new Map();
86+
const alwaysArray: Array<Target> = [];
6787

6888
let state = {
6989
changeCount: -1,
@@ -73,12 +93,27 @@ export function start<Arguments extends Array<any>>(
7393
resolved: null as Promise<Array<any>> | null,
7494
};
7595

96+
function processTarget(target: Target): boolean {
97+
if ('type' in target) {
98+
const result = target.cond();
99+
if (result) {
100+
transitionTo(target.target);
101+
return true;
102+
}
103+
} else {
104+
transitionTo(target);
105+
return true;
106+
}
107+
108+
return false;
109+
}
110+
76111
function receive(event: string, count: number) {
77112
if (count !== state.changeCount) return;
78113

79114
const target = eventsMap.get(event);
80115
if (target) {
81-
transitionTo(target);
116+
processTarget(target);
82117
}
83118
}
84119

@@ -93,6 +128,7 @@ export function start<Arguments extends Array<any>>(
93128
state.exitActions.splice(0, Infinity);
94129
state.resolved = null;
95130
eventsMap.clear();
131+
alwaysArray.splice(0, Infinity);
96132

97133
const results: Array<any> = [];
98134

@@ -114,6 +150,8 @@ export function start<Arguments extends Array<any>>(
114150
results.push(result);
115151
} else if (value.type === "on") {
116152
eventsMap.set(value.on, value.target);
153+
} else if (value.type === "always") {
154+
alwaysArray.push(value.target);
117155
}
118156
}
119157

@@ -127,6 +165,8 @@ export function start<Arguments extends Array<any>>(
127165
promise
128166
.then(() => receive("SUCCESS", snapshotCount))
129167
.catch(() => receive("FAILURE", snapshotCount));
168+
169+
alwaysArray.some(processTarget);
130170
}
131171

132172
transitionTo(initialGenerator);

0 commit comments

Comments
 (0)