@myriadcodelabs/uiflow 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +257 -109
- package/dist/flow.d.ts +24 -4
- package/dist/flow.d.ts.map +1 -1
- package/dist/flow.js +75 -18
- package/dist/flow.js.map +1 -1
- package/package.json +12 -3
package/README.md
CHANGED
|
@@ -1,190 +1,338 @@
|
|
|
1
1
|
# UIFlow
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Code-first flow orchestration for React.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
UIFlow helps you build multi-step UI without scattering state and transition logic across many components. You define steps in one place, and each step decides what comes next.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
- **Flexibility:** Combine UI steps and async logic steps in the same flow.
|
|
9
|
-
- **Reusability:** Share cross‑flow state through event channels.
|
|
7
|
+
## Why UIFlow
|
|
10
8
|
|
|
11
|
-
|
|
9
|
+
- Keep flow logic explicit: step names + transitions are centralized.
|
|
10
|
+
- Mix UI and async logic naturally: both are first-class steps.
|
|
11
|
+
- Share state across independent flows with channels.
|
|
12
|
+
- Stay in plain TypeScript objects, not custom DSLs.
|
|
13
|
+
|
|
14
|
+
## Mental model (60 seconds)
|
|
15
|
+
|
|
16
|
+
A flow is:
|
|
17
|
+
- `steps`: a map of step names to step definitions
|
|
18
|
+
- `start`: first step name
|
|
19
|
+
|
|
20
|
+
A step is either:
|
|
21
|
+
- UI step: `input`, `view`, `onOutput`
|
|
22
|
+
- Action step: `input`, `action`, `onOutput`
|
|
23
|
+
|
|
24
|
+
Transition rule:
|
|
25
|
+
- `onOutput` returns next step name (string) to move forward
|
|
26
|
+
- returning `void` keeps the same step and re-renders
|
|
27
|
+
|
|
28
|
+
## Install
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pnpm add @myriadcodelabs/uiflow
|
|
32
|
+
# or
|
|
33
|
+
npm i @myriadcodelabs/uiflow
|
|
34
|
+
# or
|
|
35
|
+
yarn add @myriadcodelabs/uiflow
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Imports
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
import { FlowRunner, defineFlow, createFlowChannel, type OutputHandle } from "@myriadcodelabs/uiflow";
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Use package-root imports only.
|
|
45
|
+
|
|
46
|
+
## Quick start (minimal runnable example)
|
|
12
47
|
|
|
13
48
|
```tsx
|
|
14
|
-
|
|
15
|
-
import { FlowRunner, defineFlow, createFlowChannel } from "@myriadcodelabs/uiflow";
|
|
49
|
+
"use client";
|
|
16
50
|
|
|
17
|
-
type
|
|
18
|
-
deckId: string;
|
|
19
|
-
cards: CardWithState[];
|
|
20
|
-
activeCardId: string | null;
|
|
21
|
-
};
|
|
51
|
+
import { FlowRunner, defineFlow, type OutputHandle } from "@myriadcodelabs/uiflow";
|
|
22
52
|
|
|
23
|
-
type
|
|
24
|
-
|
|
25
|
-
question: string;
|
|
26
|
-
answer: string;
|
|
27
|
-
flipped: boolean;
|
|
28
|
-
rating: "easy" | "medium" | "hard" | null;
|
|
29
|
-
};
|
|
53
|
+
type Data = { name: string };
|
|
54
|
+
type AskNameOutput = { action: "setName"; value: string } | { action: "submit" };
|
|
30
55
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
56
|
+
function AskNameView(props: {
|
|
57
|
+
input: { name: string };
|
|
58
|
+
output: OutputHandle<AskNameOutput>;
|
|
59
|
+
}) {
|
|
60
|
+
return (
|
|
61
|
+
<div>
|
|
62
|
+
<input
|
|
63
|
+
value={props.input.name}
|
|
64
|
+
onChange={(e) => props.output.emit({ action: "setName", value: e.target.value })}
|
|
65
|
+
placeholder="Your name"
|
|
66
|
+
/>
|
|
67
|
+
<button onClick={() => props.output.emit({ action: "submit" })}>Continue</button>
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
35
71
|
|
|
36
|
-
|
|
72
|
+
function DoneView(props: { input: { message: string }; output: OutputHandle<never> }) {
|
|
73
|
+
return <h2>{props.input.message}</h2>;
|
|
74
|
+
}
|
|
37
75
|
|
|
76
|
+
const onboardingFlow = defineFlow<Data>(
|
|
77
|
+
{
|
|
78
|
+
askName: {
|
|
79
|
+
input: (data) => ({ name: data.name }),
|
|
80
|
+
view: AskNameView,
|
|
81
|
+
onOutput: (data, output) => {
|
|
82
|
+
if (output.action === "setName") {
|
|
83
|
+
data.name = output.value;
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (output.action === "submit") {
|
|
87
|
+
return "done";
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
done: {
|
|
93
|
+
input: (data) => ({ message: `Welcome, ${data.name || "friend"}!` }),
|
|
94
|
+
view: DoneView,
|
|
95
|
+
onOutput: () => {},
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
{ start: "askName" }
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
export function App() {
|
|
102
|
+
return <FlowRunner flow={onboardingFlow} initialData={{ name: "" }} />;
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Practical pattern: study/review flow (real-world)
|
|
107
|
+
|
|
108
|
+
This pattern is taken from practical flashcards usage.
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
import { defineFlow } from "@myriadcodelabs/uiflow";
|
|
112
|
+
|
|
113
|
+
type Data = {
|
|
114
|
+
deckId: string;
|
|
115
|
+
flowData: {
|
|
116
|
+
cards: Array<{ id: string; flipped: boolean; rating: "easy" | "good" | "hard" | "again" | null }>;
|
|
117
|
+
activeCardId: string | null;
|
|
118
|
+
};
|
|
119
|
+
};
|
|
38
120
|
|
|
39
|
-
|
|
121
|
+
type StudyOutput =
|
|
122
|
+
| { action: "flip"; cardId: string }
|
|
123
|
+
| { action: "rate"; cardId: string; rating: "easy" | "good" | "hard" | "again" }
|
|
124
|
+
| { action: "next"; cardId: string };
|
|
125
|
+
|
|
126
|
+
export const studyFlow = defineFlow<Data>(
|
|
40
127
|
{
|
|
41
|
-
// step 1
|
|
42
128
|
fetchCards: {
|
|
43
129
|
input: (data) => ({ deckId: data.deckId }),
|
|
44
130
|
action: async ({ deckId }, data) => {
|
|
45
|
-
const cards = await
|
|
46
|
-
data.cards = cards.map((
|
|
47
|
-
data.activeCardId = null;
|
|
131
|
+
const cards = await fetchCardsListAction(deckId);
|
|
132
|
+
data.flowData.cards = (cards ?? []).map((c) => ({ id: c.id, flipped: false, rating: null }));
|
|
133
|
+
data.flowData.activeCardId = null;
|
|
48
134
|
return { ok: true };
|
|
49
135
|
},
|
|
50
136
|
onOutput: () => "decide",
|
|
51
137
|
},
|
|
52
138
|
|
|
53
|
-
// step 2
|
|
54
139
|
decide: {
|
|
55
|
-
input: (data) => ({ hasCards: data.cards.length > 0 }),
|
|
140
|
+
input: (data) => ({ hasCards: data.flowData.cards.length > 0 }),
|
|
56
141
|
action: ({ hasCards }) => hasCards,
|
|
57
|
-
onOutput: (_,
|
|
142
|
+
onOutput: (_, hasCards) => (hasCards ? "study" : "empty"),
|
|
58
143
|
},
|
|
59
|
-
// if no card present
|
|
60
|
-
noCard: {
|
|
61
|
-
input: () => ({}),
|
|
62
|
-
view: NoCardView,
|
|
63
|
-
onOutput: () => {},
|
|
64
|
-
},
|
|
65
|
-
// step 3
|
|
66
|
-
study: {
|
|
67
|
-
input: (data) => ({ cards: data.cards }),
|
|
68
|
-
view: CardView,
|
|
69
|
-
onOutput: (data, output, events) => {
|
|
70
|
-
const card = data.cards.find((c) => c.id === output.cardId);
|
|
71
|
-
if (!card) return "study";
|
|
72
144
|
|
|
145
|
+
study: {
|
|
146
|
+
input: (data) => ({ cards: data.flowData.cards, activeCardId: data.flowData.activeCardId }),
|
|
147
|
+
view: StudyCardsView,
|
|
148
|
+
onOutput: (data, output: StudyOutput, events) => {
|
|
73
149
|
if (output.action === "flip") {
|
|
74
|
-
data.activeCardId =
|
|
75
|
-
card
|
|
150
|
+
data.flowData.activeCardId = output.cardId;
|
|
151
|
+
const card = data.flowData.cards.find((c) => c.id === output.cardId);
|
|
152
|
+
if (card) card.flipped = true;
|
|
76
153
|
return "study";
|
|
77
154
|
}
|
|
78
155
|
|
|
79
156
|
if (output.action === "rate") {
|
|
80
|
-
data.activeCardId =
|
|
81
|
-
card
|
|
157
|
+
data.flowData.activeCardId = output.cardId;
|
|
158
|
+
const card = data.flowData.cards.find((c) => c.id === output.cardId);
|
|
159
|
+
if (card) card.rating = output.rating;
|
|
82
160
|
return "review";
|
|
83
161
|
}
|
|
84
162
|
|
|
85
163
|
if (output.action === "next") {
|
|
86
|
-
events?.studiedCounter.emit((
|
|
87
|
-
data.activeCardId = null;
|
|
164
|
+
events?.studiedCounter.emit((n: number) => n + 1);
|
|
165
|
+
data.flowData.activeCardId = null;
|
|
88
166
|
return "fetchCards";
|
|
89
167
|
}
|
|
90
168
|
},
|
|
91
169
|
},
|
|
92
170
|
|
|
93
|
-
// step 4: if user does review
|
|
94
171
|
review: {
|
|
95
172
|
input: (data) => ({
|
|
96
173
|
deckId: data.deckId,
|
|
97
|
-
cardId: data.activeCardId
|
|
98
|
-
rating: data.cards.find((c) => c.id === data.activeCardId)?.rating
|
|
174
|
+
cardId: data.flowData.activeCardId,
|
|
175
|
+
rating: data.flowData.cards.find((c) => c.id === data.flowData.activeCardId)?.rating,
|
|
99
176
|
}),
|
|
100
177
|
action: async ({ deckId, cardId, rating }) => {
|
|
101
|
-
await
|
|
178
|
+
await reviewCard(deckId, cardId, rating);
|
|
102
179
|
return { ok: true };
|
|
103
180
|
},
|
|
104
181
|
onOutput: (data, _, events) => {
|
|
105
|
-
events?.studiedCounter.emit((
|
|
106
|
-
data.activeCardId = null;
|
|
182
|
+
events?.studiedCounter.emit((n: number) => n + 1);
|
|
183
|
+
data.flowData.activeCardId = null;
|
|
107
184
|
return "fetchCards";
|
|
108
185
|
},
|
|
109
186
|
},
|
|
187
|
+
|
|
188
|
+
empty: {
|
|
189
|
+
input: () => ({}),
|
|
190
|
+
view: EmptyView,
|
|
191
|
+
onOutput: () => {},
|
|
192
|
+
},
|
|
110
193
|
},
|
|
111
194
|
{ start: "fetchCards" }
|
|
112
195
|
);
|
|
113
196
|
```
|
|
114
197
|
|
|
115
|
-
|
|
198
|
+
## Cross-flow communication with channels
|
|
199
|
+
|
|
200
|
+
Use channels when two independent flows need shared reactive state.
|
|
116
201
|
|
|
117
202
|
```tsx
|
|
118
|
-
|
|
119
|
-
input: { cards: CardWithState[] };
|
|
120
|
-
output: OutputHandle<ShowCardOutput>;
|
|
121
|
-
}> = ({ input, output }) => (
|
|
122
|
-
<div>
|
|
123
|
-
{input.cards.map((card) => (
|
|
124
|
-
<div key={card.id}>
|
|
125
|
-
<div>{card.question}</div>
|
|
126
|
-
{card.flipped ? <div>{card.answer}</div> : null}
|
|
127
|
-
<button onClick={() => output.emit({ cardId: card.id, action: "flip" })}>Show Answer</button>
|
|
128
|
-
<button onClick={() => output.emit({ cardId: card.id, action: "rate", rating: "easy" })}>Easy</button>
|
|
129
|
-
<button onClick={() => output.emit({ cardId: card.id, action: "rate", rating: "medium" })}>Medium</button>
|
|
130
|
-
<button onClick={() => output.emit({ cardId: card.id, action: "rate", rating: "hard" })}>Hard</button>
|
|
131
|
-
<button onClick={() => output.emit({ cardId: card.id, action: "next" })}>Next</button>
|
|
132
|
-
</div>
|
|
133
|
-
))}
|
|
134
|
-
</div>
|
|
135
|
-
);
|
|
203
|
+
"use client";
|
|
136
204
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
205
|
+
import { useMemo } from "react";
|
|
206
|
+
import { createFlowChannel, FlowRunner } from "@myriadcodelabs/uiflow";
|
|
207
|
+
|
|
208
|
+
export function FlashcardsScreen({ deckId }: { deckId: string }) {
|
|
209
|
+
const studiedCounter = useMemo(() => createFlowChannel<number>(0), []);
|
|
210
|
+
const channels = useMemo(() => ({ studiedCounter }), [studiedCounter]);
|
|
142
211
|
|
|
143
|
-
```tsx
|
|
144
|
-
export function App() {
|
|
145
212
|
return (
|
|
146
|
-
|
|
147
|
-
flow={
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
213
|
+
<>
|
|
214
|
+
<FlowRunner flow={counterFlow} initialData={{}} eventChannels={channels} />
|
|
215
|
+
<FlowRunner
|
|
216
|
+
flow={studyFlow}
|
|
217
|
+
initialData={{ deckId, flowData: { cards: [], activeCardId: null } }}
|
|
218
|
+
eventChannels={channels}
|
|
219
|
+
/>
|
|
220
|
+
</>
|
|
151
221
|
);
|
|
152
222
|
}
|
|
153
223
|
```
|
|
154
224
|
|
|
155
|
-
## API
|
|
225
|
+
## API reference
|
|
226
|
+
|
|
227
|
+
### `defineFlow(steps, { start })`
|
|
228
|
+
- Validates `start` exists in `steps`.
|
|
229
|
+
- Supports optional `channelTransitions` mapping (`channelKey -> resolver`).
|
|
230
|
+
- A resolver receives `{ data, currentStep, events, channelKey }` and returns `nextStep | void` (sync/async).
|
|
231
|
+
- Returns flow definition consumed by `FlowRunner`.
|
|
232
|
+
|
|
233
|
+
Example:
|
|
156
234
|
|
|
157
|
-
|
|
235
|
+
```ts
|
|
236
|
+
const flow = defineFlow(
|
|
237
|
+
{
|
|
238
|
+
fetchList: { /* ... */ },
|
|
239
|
+
showList: { /* ... */ },
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
start: "fetchList",
|
|
243
|
+
channelTransitions: {
|
|
244
|
+
refresh: ({ events, currentStep }) => {
|
|
245
|
+
const refreshCount = events?.refresh.get() ?? 0;
|
|
246
|
+
if (refreshCount > 0 && currentStep !== "fetchList") return "fetchList";
|
|
247
|
+
return;
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
}
|
|
251
|
+
);
|
|
252
|
+
```
|
|
158
253
|
|
|
159
|
-
|
|
160
|
-
Runs a flow and renders UI steps.
|
|
254
|
+
### `FlowRunner`
|
|
161
255
|
|
|
162
256
|
```tsx
|
|
163
257
|
<FlowRunner flow={flow} initialData={initialData} eventChannels={channels} />
|
|
164
258
|
```
|
|
165
259
|
|
|
166
|
-
|
|
167
|
-
- `
|
|
168
|
-
- `
|
|
260
|
+
Props:
|
|
261
|
+
- `flow`: flow definition
|
|
262
|
+
- `initialData`: mutable per-flow data object
|
|
263
|
+
- `eventChannels?`: optional channels map
|
|
264
|
+
- `eventChannelsStrategy?`: `"sticky"` (default) or `"replace"`
|
|
265
|
+
|
|
266
|
+
### `createFlowChannel(initial)`
|
|
267
|
+
Creates channel with:
|
|
268
|
+
- `get()`
|
|
269
|
+
- `emit(update)`
|
|
270
|
+
- `subscribe(listener)`
|
|
271
|
+
|
|
272
|
+
### `OutputHandle<O>`
|
|
273
|
+
UI steps emit events with:
|
|
274
|
+
- `output.emit(payload)`
|
|
275
|
+
|
|
276
|
+
## How to keep flows manageable
|
|
277
|
+
|
|
278
|
+
1. Keep views dumb: render from `input`, emit intent via `output.emit`.
|
|
279
|
+
2. Keep transition logic in `onOutput` only.
|
|
280
|
+
3. Use discriminated unions for UI output types.
|
|
281
|
+
4. Co-locate domain state (example: card + flipped + rating in one structure).
|
|
282
|
+
5. Use helper functions for repeated state ops.
|
|
283
|
+
6. Split long flows into focused steps (`fetch`, `decide`, `view`, `commit`).
|
|
284
|
+
|
|
285
|
+
## Important runtime behavior
|
|
286
|
+
|
|
287
|
+
1. A step is treated as action step when it has `action` and does not have `view`.
|
|
288
|
+
2. Action step runs automatically when it becomes current.
|
|
289
|
+
3. `FlowRunner` normalizes channels before subscribing:
|
|
290
|
+
- `"sticky"` (default): keeps first-seen channel instance per key.
|
|
291
|
+
- `"replace"`: uses the latest incoming channel instances.
|
|
292
|
+
4. Channel emissions trigger re-render for subscribed runners.
|
|
293
|
+
5. If `channelTransitions[channelKey]` exists, channel `emit` runs that resolver and transitions when a valid step is returned.
|
|
294
|
+
6. Errors in `onOutput`, action steps, or channel transition resolvers are logged (`console.error`) and not rethrown.
|
|
295
|
+
7. Returning unknown step or `void` does not change current step.
|
|
296
|
+
8. `initialData` is shallow-copied at runner initialization.
|
|
297
|
+
|
|
298
|
+
## Pitfalls to avoid
|
|
299
|
+
|
|
300
|
+
1. Creating channel instances directly in render can reset channel value if keys change or if using `"replace"` strategy.
|
|
301
|
+
2. Rebuilding `eventChannels` object each render is safe; `FlowRunner` deduplicates equivalent maps internally.
|
|
302
|
+
3. Using `output.done(...)` instead of `output.emit(...)`.
|
|
303
|
+
4. Mixing `view` and `action` in the same step.
|
|
304
|
+
5. Returning transition targets that do not exist.
|
|
305
|
+
6. Using static values in `channelTransitions`; each channel entry must be a resolver function.
|
|
306
|
+
|
|
307
|
+
## Next.js notes
|
|
308
|
+
|
|
309
|
+
- `FlowRunner` and UI step views should be in client components.
|
|
310
|
+
- Add `"use client"` at the top where needed.
|
|
311
|
+
- Server actions can be called inside action steps.
|
|
312
|
+
|
|
313
|
+
## FAQ
|
|
314
|
+
|
|
315
|
+
### Why not just `useState` + `useEffect`?
|
|
169
316
|
|
|
170
|
-
|
|
317
|
+
You can for simple screens. UIFlow is useful when screens become multi-step and transitions/side-effects spread across components.
|
|
171
318
|
|
|
172
|
-
|
|
173
|
-
Creates a flow definition from a steps map and a required `start` step name.
|
|
319
|
+
### Is flow data immutable?
|
|
174
320
|
|
|
175
|
-
|
|
321
|
+
No. Flow data is mutable by design inside step handlers.
|
|
176
322
|
|
|
177
|
-
|
|
178
|
-
Creates a shared channel for cross‑flow communication.
|
|
323
|
+
### Can I have multiple flows on one page?
|
|
179
324
|
|
|
180
|
-
|
|
181
|
-
- `FlowChannel.emit(update: T | (prev: T) => T): void` — update value and notify subscribers.
|
|
182
|
-
- `FlowChannel.subscribe(listener: () => void): () => void` — listen for changes.
|
|
325
|
+
Yes. Use channels when they need to communicate.
|
|
183
326
|
|
|
327
|
+
## Complete checklist before shipping
|
|
184
328
|
|
|
185
|
-
|
|
329
|
+
1. `start` exists and all transitions target valid step keys.
|
|
330
|
+
2. UI outputs are typed unions.
|
|
331
|
+
3. Views only emit intent.
|
|
332
|
+
4. Async work is in action steps.
|
|
333
|
+
5. Channels are stable and reused.
|
|
334
|
+
6. No internal-path imports.
|
|
186
335
|
|
|
187
|
-
|
|
188
|
-
Used by UI steps to emit output back into the flow.
|
|
336
|
+
## License
|
|
189
337
|
|
|
190
|
-
|
|
338
|
+
MIT
|
package/dist/flow.d.ts
CHANGED
|
@@ -12,9 +12,16 @@ export declare function createFlowChannel<T>(initial: T): FlowChannel<T>;
|
|
|
12
12
|
* You can refine this to a generic later, e.g. <D>.
|
|
13
13
|
*/
|
|
14
14
|
export type FlowData = Record<string, any>;
|
|
15
|
+
export interface ChannelTransitionContext<D extends FlowData = FlowData> {
|
|
16
|
+
data: D;
|
|
17
|
+
currentStep: string;
|
|
18
|
+
events?: EventChannels;
|
|
19
|
+
channelKey: string;
|
|
20
|
+
}
|
|
21
|
+
export type ChannelTransitionResolver<D extends FlowData = FlowData> = (context: ChannelTransitionContext<D>) => string | void | Promise<string | void>;
|
|
15
22
|
/**
|
|
16
23
|
* Output handle given to UI components.
|
|
17
|
-
* They call output.
|
|
24
|
+
* They call output.emit(...) when they're finished.
|
|
18
25
|
*/
|
|
19
26
|
export interface OutputHandle<O = any> {
|
|
20
27
|
emit: (output: O) => void;
|
|
@@ -23,7 +30,7 @@ export interface OutputHandle<O = any> {
|
|
|
23
30
|
* UI step:
|
|
24
31
|
* - Prepares `input` from `data`
|
|
25
32
|
* - Renders `view`
|
|
26
|
-
* - Receives `output` from the component via output.
|
|
33
|
+
* - Receives `output` from the component via output.emit()
|
|
27
34
|
* - `onOutput` decides next step and can mutate data
|
|
28
35
|
*/
|
|
29
36
|
export interface UiStep<D extends FlowData = FlowData, I = any, O = any> {
|
|
@@ -63,21 +70,34 @@ export type FlowSteps<D extends FlowData = FlowData> = Record<string, FlowStep<D
|
|
|
63
70
|
export interface FlowDefinition<D extends FlowData = FlowData> {
|
|
64
71
|
steps: FlowSteps<D>;
|
|
65
72
|
start: string;
|
|
73
|
+
channelTransitions?: Record<string, ChannelTransitionResolver<D>>;
|
|
66
74
|
}
|
|
67
75
|
/**
|
|
68
76
|
* Options when defining a flow.
|
|
69
77
|
*/
|
|
70
|
-
export interface DefineFlowOptions {
|
|
78
|
+
export interface DefineFlowOptions<D extends FlowData = FlowData> {
|
|
71
79
|
start: string;
|
|
80
|
+
/**
|
|
81
|
+
* Optional channel transition mapping.
|
|
82
|
+
* Each channel key maps to a resolver function with conditional logic
|
|
83
|
+
* that returns target step name (or void to stay).
|
|
84
|
+
*/
|
|
85
|
+
channelTransitions?: Record<string, ChannelTransitionResolver<D>>;
|
|
72
86
|
}
|
|
73
87
|
/**
|
|
74
88
|
* Main entry point to define a flow.
|
|
75
89
|
*/
|
|
76
|
-
export declare function defineFlow<D extends FlowData = FlowData>(steps: FlowSteps<D>, options: DefineFlowOptions): FlowDefinition<D>;
|
|
90
|
+
export declare function defineFlow<D extends FlowData = FlowData>(steps: FlowSteps<D>, options: DefineFlowOptions<D>): FlowDefinition<D>;
|
|
77
91
|
export interface FlowRunnerProps<D extends FlowData = FlowData> {
|
|
78
92
|
flow: FlowDefinition<D>;
|
|
79
93
|
initialData: D;
|
|
80
94
|
eventChannels?: EventChannels;
|
|
95
|
+
/**
|
|
96
|
+
* How FlowRunner treats incoming eventChannels across parent re-renders.
|
|
97
|
+
* - "sticky" (default): keep first-seen channel instance per key; ignore replacements for existing keys.
|
|
98
|
+
* - "replace": accept incoming channels as source of truth.
|
|
99
|
+
*/
|
|
100
|
+
eventChannelsStrategy?: "sticky" | "replace";
|
|
81
101
|
}
|
|
82
102
|
/**
|
|
83
103
|
* FlowRunner:
|
package/dist/flow.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"flow.d.ts","sourceRoot":"","sources":["../src/flow.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAsC,MAAM,OAAO,CAAC;AAS3D,MAAM,MAAM,OAAO,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC;AAE9C,MAAM,WAAW,WAAW,CAAC,CAAC;IAE1B,GAAG,EAAE,MAAM,CAAC,CAAC;IAGb,IAAI,EAAE,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC;IAGnC,SAAS,EAAE,CAAC,QAAQ,EAAE,MAAM,IAAI,KAAK,MAAM,IAAI,CAAC;CACnD;AAID,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC;AAI7D,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,GAAG,WAAW,CAAC,CAAC,CAAC,CAiB/D;AAMD;;;GAGG;AACH,MAAM,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;AAE3C;;;GAGG;AACH,MAAM,WAAW,YAAY,CAAC,CAAC,GAAG,GAAG;IACjC,IAAI,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,IAAI,CAAC;CAC7B;AAED;;;;;;GAMG;AACH,MAAM,WAAW,MAAM,CAAC,CAAC,SAAS,QAAQ,GAAG,QAAQ,EAAE,CAAC,GAAG,GAAG,EAAE,CAAC,GAAG,GAAG;IACnE,KAAK,EAAE,CAAC,IAAI,EAAE,CAAC,EAAE,MAAM,CAAC,EAAE,aAAa,KAAK,CAAC,CAAC;IAC9C,IAAI,EAAE,KAAK,CAAC,aAAa,CAAC;QAAE,KAAK,EAAE,CAAC,CAAC;QAAC,MAAM,EAAE,YAAY,CAAC,CAAC,CAAC,CAAA;KAAE,CAAC,CAAC;IACjE,QAAQ,EAAE,CAAC,IAAI,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,CAAC,EAAE,aAAa,KAAK,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CACpG;AAED;;;;;;GAMG;AACH,MAAM,WAAW,UAAU,CAAC,CAAC,SAAS,QAAQ,GAAG,QAAQ,EAAE,CAAC,GAAG,GAAG,EAAE,CAAC,GAAG,GAAG;IACvE,KAAK,EAAE,CAAC,IAAI,EAAE,CAAC,EAAE,MAAM,CAAC,EAAE,aAAa,KAAK,CAAC,CAAC;IAC9C,MAAM,EAAE,CAAC,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,MAAM,CAAC,EAAE,aAAa,KAAK,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IACtE,QAAQ,EAAE,CAAC,IAAI,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,CAAC,EAAE,aAAa,KAAK,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CACpG;AAED;;;;;GAKG;AACH,MAAM,MAAM,QAAQ,CAAC,CAAC,SAAS,QAAQ,GAAG,QAAQ,IAC5C,MAAM,CAAC,CAAC,EAAE,GAAG,EAAE,GAAG,CAAC,GACnB,UAAU,CAAC,CAAC,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC;AAE9B;;GAEG;AACH,MAAM,MAAM,SAAS,CAAC,CAAC,SAAS,QAAQ,GAAG,QAAQ,IAAI,MAAM,CACzD,MAAM,EACN,QAAQ,CAAC,CAAC,CAAC,CACd,CAAC;AAEF;;GAEG;AACH,MAAM,WAAW,cAAc,CAAC,CAAC,SAAS,QAAQ,GAAG,QAAQ;IACzD,KAAK,EAAE,SAAS,CAAC,CAAC,CAAC,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"flow.d.ts","sourceRoot":"","sources":["../src/flow.tsx"],"names":[],"mappings":"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"}
|
package/dist/flow.js
CHANGED
|
@@ -30,6 +30,7 @@ export function defineFlow(steps, options) {
|
|
|
30
30
|
return {
|
|
31
31
|
steps,
|
|
32
32
|
start: options.start,
|
|
33
|
+
channelTransitions: options.channelTransitions,
|
|
33
34
|
};
|
|
34
35
|
}
|
|
35
36
|
/**
|
|
@@ -39,10 +40,38 @@ export function defineFlow(steps, options) {
|
|
|
39
40
|
* - renders UI steps
|
|
40
41
|
*/
|
|
41
42
|
export function FlowRunner(props) {
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
const
|
|
45
|
-
|
|
43
|
+
const { flow, initialData, eventChannels, eventChannelsStrategy = "sticky", } = props;
|
|
44
|
+
const resolvedChannelsRef = useRef(undefined);
|
|
45
|
+
const getResolvedChannels = () => {
|
|
46
|
+
const prev = resolvedChannelsRef.current;
|
|
47
|
+
const incoming = eventChannels;
|
|
48
|
+
if (!incoming) {
|
|
49
|
+
resolvedChannelsRef.current = undefined;
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
const incomingEntries = Object.entries(incoming);
|
|
53
|
+
let candidate;
|
|
54
|
+
if (eventChannelsStrategy === "sticky") {
|
|
55
|
+
candidate = {};
|
|
56
|
+
for (const [key, channel] of incomingEntries) {
|
|
57
|
+
candidate[key] = prev?.[key] ?? channel;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
candidate = incoming;
|
|
62
|
+
}
|
|
63
|
+
if (prev) {
|
|
64
|
+
const prevKeys = Object.keys(prev);
|
|
65
|
+
const candidateKeys = Object.keys(candidate);
|
|
66
|
+
if (prevKeys.length === candidateKeys.length &&
|
|
67
|
+
candidateKeys.every((k) => prev[k] === candidate[k])) {
|
|
68
|
+
return prev;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
resolvedChannelsRef.current = candidate;
|
|
72
|
+
return candidate;
|
|
73
|
+
};
|
|
74
|
+
const resolvedEventChannels = getResolvedChannels();
|
|
46
75
|
// We keep data and currentStep in state so React re-renders on change.
|
|
47
76
|
const [state, setState] = useState({
|
|
48
77
|
currentStep: flow.start,
|
|
@@ -54,15 +83,6 @@ export function FlowRunner(props) {
|
|
|
54
83
|
// This state is never used directly.
|
|
55
84
|
// It only exists to force a re-render when event channels change.
|
|
56
85
|
const [_tick, setTick] = useState(0);
|
|
57
|
-
// NEW:
|
|
58
|
-
// Subscribe to every provided channel.
|
|
59
|
-
// When any channel emits, we trigger a re-render of this FlowRunner.
|
|
60
|
-
useEffect(() => {
|
|
61
|
-
if (!eventChannels)
|
|
62
|
-
return;
|
|
63
|
-
const unsubs = Object.values(eventChannels).map((ch) => ch.subscribe(() => setTick((x) => x + 1)));
|
|
64
|
-
return () => unsubs.forEach((u) => u());
|
|
65
|
-
}, []);
|
|
66
86
|
const { currentStep, data } = state;
|
|
67
87
|
const applyTransition = (nextStepName) => {
|
|
68
88
|
if (!isMountedRef.current)
|
|
@@ -82,6 +102,43 @@ export function FlowRunner(props) {
|
|
|
82
102
|
}));
|
|
83
103
|
}
|
|
84
104
|
};
|
|
105
|
+
const runChannelTransition = async (channelKey) => {
|
|
106
|
+
const transition = flow.channelTransitions?.[channelKey];
|
|
107
|
+
if (!transition) {
|
|
108
|
+
setTick((x) => x + 1);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
const nextStep = await transition({
|
|
113
|
+
data,
|
|
114
|
+
currentStep,
|
|
115
|
+
events: resolvedEventChannels,
|
|
116
|
+
channelKey,
|
|
117
|
+
});
|
|
118
|
+
if (nextStep) {
|
|
119
|
+
applyTransition(nextStep);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
setTick((x) => x + 1);
|
|
123
|
+
}
|
|
124
|
+
catch (e) {
|
|
125
|
+
console.error("FlowRunner channel transition error:", e);
|
|
126
|
+
setTick((x) => x + 1);
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
// Subscribe to every provided channel and keep subscriptions in sync
|
|
130
|
+
// with the current eventChannels prop.
|
|
131
|
+
useEffect(() => {
|
|
132
|
+
if (!resolvedEventChannels)
|
|
133
|
+
return;
|
|
134
|
+
const unsubs = Object.entries(resolvedEventChannels).map(([channelKey, ch]) => ch.subscribe(() => {
|
|
135
|
+
void runChannelTransition(channelKey);
|
|
136
|
+
}));
|
|
137
|
+
return () => unsubs.forEach((u) => u());
|
|
138
|
+
// applyTransition and runChannelTransition are recreated each render;
|
|
139
|
+
// subscription identity is driven by channels map.
|
|
140
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
141
|
+
}, [flow.channelTransitions, resolvedEventChannels]);
|
|
85
142
|
useEffect(() => {
|
|
86
143
|
return () => {
|
|
87
144
|
isMountedRef.current = false;
|
|
@@ -97,9 +154,9 @@ export function FlowRunner(props) {
|
|
|
97
154
|
(async () => {
|
|
98
155
|
try {
|
|
99
156
|
setBusy(true);
|
|
100
|
-
const input = actionStep.input(state.data,
|
|
101
|
-
const output = await actionStep.action(input, state.data,
|
|
102
|
-
const next = await actionStep.onOutput(state.data, output,
|
|
157
|
+
const input = actionStep.input(state.data, resolvedEventChannels);
|
|
158
|
+
const output = await actionStep.action(input, state.data, resolvedEventChannels);
|
|
159
|
+
const next = await actionStep.onOutput(state.data, output, resolvedEventChannels);
|
|
103
160
|
applyTransition(next);
|
|
104
161
|
}
|
|
105
162
|
catch (e) {
|
|
@@ -132,11 +189,11 @@ export function FlowRunner(props) {
|
|
|
132
189
|
// -----------------------
|
|
133
190
|
const uiStep = step;
|
|
134
191
|
const ViewComponent = uiStep.view;
|
|
135
|
-
const input = uiStep.input(data,
|
|
192
|
+
const input = uiStep.input(data, resolvedEventChannels);
|
|
136
193
|
const outputHandle = {
|
|
137
194
|
emit: async (output) => {
|
|
138
195
|
try {
|
|
139
|
-
const next = await uiStep.onOutput(data, output,
|
|
196
|
+
const next = await uiStep.onOutput(data, output, resolvedEventChannels);
|
|
140
197
|
applyTransition(next);
|
|
141
198
|
}
|
|
142
199
|
catch (e) {
|
package/dist/flow.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"flow.js","sourceRoot":"","sources":["../src/flow.tsx"],"names":[],"mappings":";AAAA,uDAAuD;AACvD,gDAAgD;AAEhD,eAAe;AACf,OAAO,KAAK,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AA0B3D,wCAAwC;AACxC,6EAA6E;AAC7E,MAAM,UAAU,iBAAiB,CAAI,OAAU;IAC3C,IAAI,KAAK,GAAG,OAAO,CAAC;IACpB,MAAM,SAAS,GAAG,IAAI,GAAG,EAAc,CAAC;IAExC,OAAO;QACH,GAAG,EAAE,GAAG,EAAE,CAAC,KAAK;QAEhB,IAAI,EAAE,CAAC,MAAkB,EAAE,EAAE;YACzB,KAAK,GAAG,OAAO,MAAM,KAAK,UAAU,CAAC,CAAC,CAAE,MAAsB,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;YAC/E,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,yBAAyB;QAC5D,CAAC;QAED,SAAS,EAAE,CAAC,QAAoB,EAAE,EAAE;YAChC,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;YACxB,OAAO,GAAG,EAAE,CAAC,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QAC5C,CAAC;KACJ,CAAC;AACN,CAAC;
|
|
1
|
+
{"version":3,"file":"flow.js","sourceRoot":"","sources":["../src/flow.tsx"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@myriadcodelabs/uiflow",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Explicit, code-first UI flow orchestration for React.",
|
|
5
5
|
"keywords": [],
|
|
6
6
|
"author": "Muhammad Ismail Khan",
|
|
@@ -9,8 +9,16 @@
|
|
|
9
9
|
"react": ">=18"
|
|
10
10
|
},
|
|
11
11
|
"devDependencies": {
|
|
12
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
13
|
+
"@testing-library/react": "^16.3.2",
|
|
14
|
+
"@testing-library/user-event": "^14.6.1",
|
|
12
15
|
"@types/react": "^19.2.10",
|
|
13
|
-
"
|
|
16
|
+
"@types/react-dom": "^19.2.3",
|
|
17
|
+
"jsdom": "^28.0.0",
|
|
18
|
+
"react": "^19.2.4",
|
|
19
|
+
"react-dom": "^19.2.4",
|
|
20
|
+
"typescript": "^5.9.3",
|
|
21
|
+
"vitest": "^4.0.18"
|
|
14
22
|
},
|
|
15
23
|
"type": "module",
|
|
16
24
|
"files": [
|
|
@@ -25,7 +33,8 @@
|
|
|
25
33
|
}
|
|
26
34
|
},
|
|
27
35
|
"scripts": {
|
|
28
|
-
"test": "
|
|
36
|
+
"test": "vitest run",
|
|
37
|
+
"test:watch": "vitest",
|
|
29
38
|
"clean": "rimraf dist",
|
|
30
39
|
"build": "pnpm run clean && tsc"
|
|
31
40
|
}
|