@fieldtripjs/react 0.0.0-stage → 0.1.0
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/LICENSE +21 -0
- package/README.md +8 -2
- package/dist/context.d.ts +10 -0
- package/dist/context.js +17 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/use-flow.d.ts +30 -0
- package/dist/use-flow.js +75 -0
- package/package.json +56 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Martin Aarhus Gregersen
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @fieldtripjs/react
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Headless React bindings for [@fieldtripjs/core](https://www.npmjs.com/package/@fieldtripjs/core): `useFlow` and `createFlowContext`. State and navigation from fieldtrip, UI from you.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
pnpm add @fieldtripjs/core @fieldtripjs/react
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
See the [main README](https://github.com/aarhusgregersen/fieldtrip#readme) for docs.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { AnyFlow } from "@fieldtripjs/core";
|
|
2
|
+
import { type ReactNode } from "react";
|
|
3
|
+
import { type FlowController, type UseFlowOptions } from "./use-flow.js";
|
|
4
|
+
export interface FlowContext<FL extends AnyFlow> {
|
|
5
|
+
readonly FlowProvider: (props: UseFlowOptions<FL> & {
|
|
6
|
+
readonly children: ReactNode;
|
|
7
|
+
}) => ReactNode;
|
|
8
|
+
readonly useFlowContext: () => FlowController<FL>;
|
|
9
|
+
}
|
|
10
|
+
export declare function createFlowContext<FL extends AnyFlow>(flow: FL): FlowContext<FL>;
|
package/dist/context.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useContext } from "react";
|
|
3
|
+
import { useFlow } from "./use-flow.js";
|
|
4
|
+
// One context per flow, so step components deep in the tree get the flow's own types.
|
|
5
|
+
export function createFlowContext(flow) {
|
|
6
|
+
const Context = createContext(null);
|
|
7
|
+
function FlowProvider({ children, ...options }) {
|
|
8
|
+
return _jsx(Context.Provider, { value: useFlow(flow, options), children: children });
|
|
9
|
+
}
|
|
10
|
+
function useFlowContext() {
|
|
11
|
+
const controller = useContext(Context);
|
|
12
|
+
if (!controller)
|
|
13
|
+
throw new Error("useFlowContext must be used inside its FlowProvider");
|
|
14
|
+
return controller;
|
|
15
|
+
}
|
|
16
|
+
return { FlowProvider, useFlowContext };
|
|
17
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type AnyFlow, type FieldIssues, type FieldName, type FlowState, type FlowValues, type StepName } from "@fieldtripjs/core";
|
|
2
|
+
export interface UseFlowOptions<FL extends AnyFlow> {
|
|
3
|
+
readonly initialState?: FlowState<FL>;
|
|
4
|
+
readonly onChange?: (state: FlowState<FL>) => void;
|
|
5
|
+
}
|
|
6
|
+
export interface FieldProps<N extends string = string> {
|
|
7
|
+
readonly name: N;
|
|
8
|
+
readonly value: unknown;
|
|
9
|
+
readonly issues: readonly string[];
|
|
10
|
+
readonly visible: boolean;
|
|
11
|
+
readonly onChange: (value: unknown) => void;
|
|
12
|
+
}
|
|
13
|
+
export interface FlowController<FL extends AnyFlow> {
|
|
14
|
+
readonly state: FlowState<FL>;
|
|
15
|
+
readonly step: StepName<FL>;
|
|
16
|
+
readonly path: readonly StepName<FL>[];
|
|
17
|
+
readonly values: FlowValues<FL>;
|
|
18
|
+
readonly inputs: Readonly<Record<string, unknown>>;
|
|
19
|
+
readonly issues: FieldIssues;
|
|
20
|
+
readonly visibleFields: readonly FieldName<FL>[];
|
|
21
|
+
readonly isComplete: boolean;
|
|
22
|
+
readonly isPending: boolean;
|
|
23
|
+
readonly canGoBack: boolean;
|
|
24
|
+
readonly setInput: (name: FieldName<FL>, value: unknown) => void;
|
|
25
|
+
readonly field: <N extends FieldName<FL>>(name: N) => FieldProps<N>;
|
|
26
|
+
readonly next: () => Promise<boolean>;
|
|
27
|
+
readonly back: () => void;
|
|
28
|
+
readonly reset: () => void;
|
|
29
|
+
}
|
|
30
|
+
export declare function useFlow<FL extends AnyFlow>(flow: FL, options?: UseFlowOptions<FL>): FlowController<FL>;
|
package/dist/use-flow.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { back as backState, next as nextState, path as pathOf, result, setInputs, start, visibleFields as visibleFieldsOf, } from "@fieldtripjs/core";
|
|
2
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
+
export function useFlow(flow, options = {}) {
|
|
4
|
+
const [state, setState] = useState(() => options.initialState ?? start(flow));
|
|
5
|
+
const [issues, setIssues] = useState({});
|
|
6
|
+
const [isPending, setPending] = useState(false);
|
|
7
|
+
const latest = useRef(state);
|
|
8
|
+
const onChange = useRef(options.onChange);
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
onChange.current = options.onChange;
|
|
11
|
+
});
|
|
12
|
+
const commit = useCallback((next) => {
|
|
13
|
+
latest.current = next;
|
|
14
|
+
setState(next);
|
|
15
|
+
onChange.current?.(next);
|
|
16
|
+
}, []);
|
|
17
|
+
const setInput = useCallback((name, value) => {
|
|
18
|
+
commit(setInputs(flow, latest.current, { [name]: value }));
|
|
19
|
+
setIssues(({ [name]: _cleared, ...rest }) => rest);
|
|
20
|
+
}, [flow, commit]);
|
|
21
|
+
const next = useCallback(async () => {
|
|
22
|
+
const submitted = latest.current;
|
|
23
|
+
setPending(true);
|
|
24
|
+
try {
|
|
25
|
+
const outcome = await nextState(flow, submitted);
|
|
26
|
+
// Input that arrived while validating would be silently overwritten by the stale result.
|
|
27
|
+
if (latest.current !== submitted)
|
|
28
|
+
return false;
|
|
29
|
+
if (!outcome.ok) {
|
|
30
|
+
setIssues(outcome.issues);
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
setIssues({});
|
|
34
|
+
commit(outcome.state);
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
finally {
|
|
38
|
+
setPending(false);
|
|
39
|
+
}
|
|
40
|
+
}, [flow, commit]);
|
|
41
|
+
const back = useCallback(() => {
|
|
42
|
+
setIssues({});
|
|
43
|
+
commit(backState(flow, latest.current));
|
|
44
|
+
}, [flow, commit]);
|
|
45
|
+
const reset = useCallback(() => {
|
|
46
|
+
setIssues({});
|
|
47
|
+
commit(start(flow));
|
|
48
|
+
}, [flow, commit]);
|
|
49
|
+
const inputs = state.inputs[state.current] ?? {};
|
|
50
|
+
const visibleFields = useMemo(() => visibleFieldsOf(flow, state), [flow, state]);
|
|
51
|
+
const field = useCallback((name) => ({
|
|
52
|
+
name,
|
|
53
|
+
value: inputs[name],
|
|
54
|
+
issues: issues[name] ?? [],
|
|
55
|
+
visible: visibleFields.includes(name),
|
|
56
|
+
onChange: (value) => setInput(name, value),
|
|
57
|
+
}), [inputs, issues, visibleFields, setInput]);
|
|
58
|
+
return useMemo(() => ({
|
|
59
|
+
state,
|
|
60
|
+
step: state.current,
|
|
61
|
+
path: pathOf(state),
|
|
62
|
+
values: result(flow, state),
|
|
63
|
+
inputs,
|
|
64
|
+
issues,
|
|
65
|
+
visibleFields,
|
|
66
|
+
isComplete: state.status === "complete",
|
|
67
|
+
isPending,
|
|
68
|
+
canGoBack: state.history.length > 0 || state.status === "complete",
|
|
69
|
+
setInput,
|
|
70
|
+
field,
|
|
71
|
+
next,
|
|
72
|
+
back,
|
|
73
|
+
reset,
|
|
74
|
+
}), [flow, state, inputs, issues, visibleFields, isPending, setInput, field, next, back, reset]);
|
|
75
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,59 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fieldtripjs/react",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Headless React bindings for @fieldtripjs/core.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Martin Aarhus Gregersen",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/aarhusgregersen/fieldtrip.git",
|
|
11
|
+
"directory": "packages/react"
|
|
12
|
+
},
|
|
13
|
+
"homepage": "https://github.com/aarhusgregersen/fieldtrip#readme",
|
|
14
|
+
"sideEffects": false,
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"import": "./dist/index.js"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist"
|
|
23
|
+
],
|
|
24
|
+
"peerDependencies": {
|
|
25
|
+
"react": ">=18"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@fieldtripjs/core": "0.1.0"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@testing-library/react": "^16.3.3",
|
|
32
|
+
"@types/react": "^19.3.0",
|
|
33
|
+
"@types/react-dom": "^19.3.0",
|
|
34
|
+
"jsdom": "^30.1.1",
|
|
35
|
+
"react": ">=18",
|
|
36
|
+
"react-dom": "^19.3.0",
|
|
37
|
+
"typescript": "^7.0.2",
|
|
38
|
+
"vitest": "^5.0.3",
|
|
39
|
+
"zod": "^4.6.5"
|
|
40
|
+
},
|
|
41
|
+
"publishConfig": {
|
|
42
|
+
"access": "public"
|
|
43
|
+
},
|
|
44
|
+
"keywords": [
|
|
45
|
+
"forms",
|
|
46
|
+
"wizard",
|
|
47
|
+
"multi-step",
|
|
48
|
+
"state-machine",
|
|
49
|
+
"typescript",
|
|
50
|
+
"standard-schema",
|
|
51
|
+
"react",
|
|
52
|
+
"hooks"
|
|
53
|
+
],
|
|
54
|
+
"scripts": {
|
|
55
|
+
"build": "tsc -p tsconfig.build.json",
|
|
56
|
+
"test": "vitest run --typecheck",
|
|
57
|
+
"typecheck": "tsc --noEmit"
|
|
58
|
+
}
|
|
6
59
|
}
|