@mmapp/react 0.1.0-alpha.1 → 0.1.0-alpha.4
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 +112 -0
- package/dist/index.d.mts +1378 -94
- package/dist/index.d.ts +1378 -94
- package/dist/index.js +1094 -1309
- package/dist/index.mjs +1038 -1296
- package/package.json +4 -3
- package/package.json.backup +0 -41
- package/src/Blueprint.ts +0 -216
- package/src/__tests__/Blueprint.test.ts +0 -106
- package/src/__tests__/action-context.test.ts +0 -166
- package/src/__tests__/actionCreators.test.ts +0 -179
- package/src/__tests__/builders.test.ts +0 -336
- package/src/__tests__/defineBlueprint-composition.test.ts +0 -106
- package/src/__tests__/factories.test.ts +0 -229
- package/src/__tests__/loader.test.ts +0 -159
- package/src/__tests__/logger.test.ts +0 -70
- package/src/__tests__/type-inference.test.ts +0 -160
- package/src/__tests__/typed-transitions.test.ts +0 -126
- package/src/__tests__/useModuleConfig.test.ts +0 -61
- package/src/actionCreators.ts +0 -132
- package/src/actions.ts +0 -547
- package/src/atoms/index.ts +0 -600
- package/src/authoring.ts +0 -92
- package/src/browser-player.ts +0 -783
- package/src/builders.ts +0 -1342
- package/src/components/ExperienceWorkflowBridge.tsx +0 -123
- package/src/components/PlayerProvider.tsx +0 -43
- package/src/components/atoms/index.tsx +0 -269
- package/src/components/index.ts +0 -36
- package/src/conditions.ts +0 -692
- package/src/config/defineBlueprint.ts +0 -329
- package/src/config/defineModel.ts +0 -753
- package/src/config/defineWorkspace.ts +0 -24
- package/src/core/WorkflowRuntime.ts +0 -153
- package/src/factories.ts +0 -425
- package/src/grammar/index.ts +0 -173
- package/src/hooks/index.ts +0 -106
- package/src/hooks/useAuth.ts +0 -288
- package/src/hooks/useChannel.ts +0 -304
- package/src/hooks/useComputed.ts +0 -154
- package/src/hooks/useDomainSubscription.ts +0 -110
- package/src/hooks/useDuringAction.ts +0 -99
- package/src/hooks/useExperienceState.ts +0 -59
- package/src/hooks/useExpressionLibrary.ts +0 -129
- package/src/hooks/useForm.ts +0 -352
- package/src/hooks/useGeolocation.ts +0 -207
- package/src/hooks/useMapView.ts +0 -259
- package/src/hooks/useMiddleware.ts +0 -291
- package/src/hooks/useModel.ts +0 -363
- package/src/hooks/useModule.ts +0 -59
- package/src/hooks/useModuleConfig.ts +0 -61
- package/src/hooks/useMutation.ts +0 -237
- package/src/hooks/useNotification.ts +0 -151
- package/src/hooks/useOnChange.ts +0 -30
- package/src/hooks/useOnEnter.ts +0 -59
- package/src/hooks/useOnEvent.ts +0 -37
- package/src/hooks/useOnExit.ts +0 -27
- package/src/hooks/useOnTransition.ts +0 -30
- package/src/hooks/usePackage.ts +0 -128
- package/src/hooks/useParams.ts +0 -33
- package/src/hooks/usePlayer.ts +0 -308
- package/src/hooks/useQuery.ts +0 -184
- package/src/hooks/useRealtimeQuery.ts +0 -222
- package/src/hooks/useRole.ts +0 -191
- package/src/hooks/useRouteParams.ts +0 -100
- package/src/hooks/useRouter.ts +0 -347
- package/src/hooks/useServerAction.ts +0 -178
- package/src/hooks/useServerState.ts +0 -284
- package/src/hooks/useToast.ts +0 -164
- package/src/hooks/useTransition.ts +0 -39
- package/src/hooks/useView.ts +0 -102
- package/src/hooks/useWhileIn.ts +0 -48
- package/src/hooks/useWorkflow.ts +0 -63
- package/src/index.ts +0 -465
- package/src/loader/experience-workflow-loader.ts +0 -192
- package/src/loader/index.ts +0 -6
- package/src/local/LocalEngine.ts +0 -388
- package/src/local/LocalEngineAdapter.ts +0 -175
- package/src/local/LocalEngineContext.ts +0 -30
- package/src/logger.ts +0 -37
- package/src/mixins.ts +0 -1160
- package/src/providers/RuntimeContext.ts +0 -20
- package/src/providers/WorkflowProvider.tsx +0 -28
- package/src/routing/instance-key.ts +0 -107
- package/src/server/transition-context.ts +0 -172
- package/src/testing/index.ts +0 -9
- package/src/testing/useBlueprintTestRunner.ts +0 -91
- package/src/testing/useGraphAnalysis.ts +0 -18
- package/src/testing/useTestRunner.ts +0 -77
- package/src/testing.ts +0 -995
- package/src/types/workflow-inference.ts +0 -158
- package/src/types.ts +0 -114
- package/tsconfig.json +0 -27
- package/vitest.config.ts +0 -8
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* useExperienceState — Reactive accessor for experience workflow state.
|
|
3
|
-
*
|
|
4
|
-
* Provides fine-grained access to the player's state with optional
|
|
5
|
-
* selector for render optimization. Components only re-render when
|
|
6
|
-
* their selected slice of state actually changes.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { useCallback, useRef } from 'react';
|
|
10
|
-
import type { PlayerHandle } from '../types';
|
|
11
|
-
|
|
12
|
-
type Selector<T> = (state: {
|
|
13
|
-
currentState: string;
|
|
14
|
-
stateData: Record<string, unknown>;
|
|
15
|
-
status: string;
|
|
16
|
-
availableTransitions: string[];
|
|
17
|
-
}) => T;
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Subscribe to a slice of player state with a selector.
|
|
21
|
-
*
|
|
22
|
-
* @example
|
|
23
|
-
* ```tsx
|
|
24
|
-
* const title = useExperienceState(player, s => s.stateData.title as string);
|
|
25
|
-
* const canAdvance = useExperienceState(player, s => s.availableTransitions.includes('advance'));
|
|
26
|
-
* ```
|
|
27
|
-
*/
|
|
28
|
-
export function useExperienceState<T>(player: PlayerHandle, selector: Selector<T>): T {
|
|
29
|
-
const selectorRef = useRef(selector);
|
|
30
|
-
selectorRef.current = selector;
|
|
31
|
-
|
|
32
|
-
const getSnapshot = useCallback(() => {
|
|
33
|
-
return selectorRef.current({
|
|
34
|
-
currentState: player.currentState,
|
|
35
|
-
stateData: player.stateData,
|
|
36
|
-
status: player.status,
|
|
37
|
-
availableTransitions: player.availableTransitions,
|
|
38
|
-
});
|
|
39
|
-
}, [player.currentState, player.stateData, player.status, player.availableTransitions]);
|
|
40
|
-
|
|
41
|
-
return getSnapshot();
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Access a single field from state_data.
|
|
46
|
-
*
|
|
47
|
-
* @example
|
|
48
|
-
* ```tsx
|
|
49
|
-
* const count = useStateField(player, 'count', 0);
|
|
50
|
-
* ```
|
|
51
|
-
*/
|
|
52
|
-
export function useStateField<T = unknown>(
|
|
53
|
-
player: PlayerHandle,
|
|
54
|
-
field: string,
|
|
55
|
-
defaultValue?: T,
|
|
56
|
-
): T {
|
|
57
|
-
const value = player.stateData[field];
|
|
58
|
-
return (value !== undefined ? value : defaultValue) as T;
|
|
59
|
-
}
|
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* useExpressionLibrary — Load an expression library by slug
|
|
3
|
-
*
|
|
4
|
-
* Fetches a workflow definition with category='expression-library' and
|
|
5
|
-
* returns its expression + metadata for use in computed fields.
|
|
6
|
-
* Supports I3 expression library system.
|
|
7
|
-
*
|
|
8
|
-
* Usage in .workflow.tsx:
|
|
9
|
-
* const { expression, functions, loading } = useExpressionLibrary('pricing/bulk-discount');
|
|
10
|
-
* // Use in computed field: call "pricing/bulk-discount"(subtotal, quantity)
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
14
|
-
|
|
15
|
-
// =============================================================================
|
|
16
|
-
// Types
|
|
17
|
-
// =============================================================================
|
|
18
|
-
|
|
19
|
-
export interface ExpressionLibraryResult {
|
|
20
|
-
/** The expression string from the library. */
|
|
21
|
-
expression: string | null;
|
|
22
|
-
/** Library name. */
|
|
23
|
-
name: string | null;
|
|
24
|
-
/** Library description. */
|
|
25
|
-
description: string | null;
|
|
26
|
-
/** Parameter signature (from metadata). */
|
|
27
|
-
params: LibraryParam[];
|
|
28
|
-
/** Return type hint (from metadata). */
|
|
29
|
-
returnType: string | null;
|
|
30
|
-
/** Whether the library is currently loading. */
|
|
31
|
-
loading: boolean;
|
|
32
|
-
/** Error from the last fetch attempt. */
|
|
33
|
-
error: Error | null;
|
|
34
|
-
/** Manually trigger a refetch. */
|
|
35
|
-
refetch: () => Promise<void>;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export interface LibraryParam {
|
|
39
|
-
name: string;
|
|
40
|
-
type?: string;
|
|
41
|
-
description?: string;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export interface ExpressionLibraryRecord {
|
|
45
|
-
id: string;
|
|
46
|
-
slug: string;
|
|
47
|
-
name: string;
|
|
48
|
-
description?: string;
|
|
49
|
-
metadata?: {
|
|
50
|
-
expression?: string;
|
|
51
|
-
params?: LibraryParam[];
|
|
52
|
-
returnType?: string;
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Expression library resolver — the runtime must provide an implementation.
|
|
58
|
-
*/
|
|
59
|
-
export interface ExpressionLibraryResolver {
|
|
60
|
-
getLibrary: (slug: string) => Promise<ExpressionLibraryRecord | null>;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// Global resolver (set by provider)
|
|
64
|
-
let _globalExpressionLibraryResolver: ExpressionLibraryResolver | null = null;
|
|
65
|
-
|
|
66
|
-
export function setExpressionLibraryResolver(resolver: ExpressionLibraryResolver | null): void {
|
|
67
|
-
_globalExpressionLibraryResolver = resolver;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// =============================================================================
|
|
71
|
-
// Hook
|
|
72
|
-
// =============================================================================
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Fetches an expression library definition by slug.
|
|
76
|
-
*
|
|
77
|
-
* @param slug - The expression library slug (e.g., 'pricing/bulk-discount').
|
|
78
|
-
* @returns Library result with expression, params, loading state, and error.
|
|
79
|
-
*/
|
|
80
|
-
export function useExpressionLibrary(slug: string | null): ExpressionLibraryResult {
|
|
81
|
-
const [record, setRecord] = useState<ExpressionLibraryRecord | null>(null);
|
|
82
|
-
const [loading, setLoading] = useState(!!slug);
|
|
83
|
-
const [error, setError] = useState<Error | null>(null);
|
|
84
|
-
const slugRef = useRef(slug);
|
|
85
|
-
slugRef.current = slug;
|
|
86
|
-
|
|
87
|
-
const fetchLibrary = useCallback(async () => {
|
|
88
|
-
const s = slugRef.current;
|
|
89
|
-
if (!s) {
|
|
90
|
-
setRecord(null);
|
|
91
|
-
setLoading(false);
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const resolver = _globalExpressionLibraryResolver;
|
|
96
|
-
if (!resolver) {
|
|
97
|
-
setError(new Error('useExpressionLibrary: No resolver configured. Wrap your app in a WorkflowProvider.'));
|
|
98
|
-
setLoading(false);
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
try {
|
|
103
|
-
setLoading(true);
|
|
104
|
-
setError(null);
|
|
105
|
-
const result = await resolver.getLibrary(s);
|
|
106
|
-
setRecord(result);
|
|
107
|
-
} catch (err) {
|
|
108
|
-
setError(err instanceof Error ? err : new Error(String(err)));
|
|
109
|
-
} finally {
|
|
110
|
-
setLoading(false);
|
|
111
|
-
}
|
|
112
|
-
}, []);
|
|
113
|
-
|
|
114
|
-
useEffect(() => {
|
|
115
|
-
fetchLibrary();
|
|
116
|
-
}, [slug, fetchLibrary]);
|
|
117
|
-
|
|
118
|
-
const handle: ExpressionLibraryResult = {
|
|
119
|
-
expression: record?.metadata?.expression ?? null,
|
|
120
|
-
name: record?.name ?? null,
|
|
121
|
-
description: record?.description ?? null,
|
|
122
|
-
params: record?.metadata?.params ?? [],
|
|
123
|
-
returnType: record?.metadata?.returnType ?? null,
|
|
124
|
-
loading,
|
|
125
|
-
error,
|
|
126
|
-
refetch: fetchLibrary,
|
|
127
|
-
};
|
|
128
|
-
return handle;
|
|
129
|
-
}
|
package/src/hooks/useForm.ts
DELETED
|
@@ -1,352 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* useForm — Form state management with validation, dirty tracking, and submit handling.
|
|
3
|
-
*
|
|
4
|
-
* Provides a complete form solution with field-level and form-level validation,
|
|
5
|
-
* dirty/touched tracking, and async submit handling.
|
|
6
|
-
*
|
|
7
|
-
* Usage in .workflow.tsx:
|
|
8
|
-
* const form = useForm({
|
|
9
|
-
* initialValues: { pickup: '', destination: '', rideType: 'standard' },
|
|
10
|
-
* validate: (values) => {
|
|
11
|
-
* const errors: Record<string, string> = {};
|
|
12
|
-
* if (!values.pickup) errors.pickup = 'Pickup location is required';
|
|
13
|
-
* if (!values.destination) errors.destination = 'Destination is required';
|
|
14
|
-
* return errors;
|
|
15
|
-
* },
|
|
16
|
-
* onSubmit: async (values) => {
|
|
17
|
-
* await createRide(values);
|
|
18
|
-
* },
|
|
19
|
-
* });
|
|
20
|
-
*
|
|
21
|
-
* <TextInput bind="pickup" {...form.register('pickup')} />
|
|
22
|
-
* <Button onClick={form.handleSubmit} disabled={!form.isValid}>
|
|
23
|
-
* Request Ride
|
|
24
|
-
* </Button>
|
|
25
|
-
*/
|
|
26
|
-
|
|
27
|
-
import { useState, useCallback, useMemo, useRef } from 'react';
|
|
28
|
-
|
|
29
|
-
// =============================================================================
|
|
30
|
-
// Types
|
|
31
|
-
// =============================================================================
|
|
32
|
-
|
|
33
|
-
/** Validation function. Returns error messages keyed by field name. */
|
|
34
|
-
export type FormValidator<T extends Record<string, unknown>> = (
|
|
35
|
-
values: T,
|
|
36
|
-
) => Record<string, string> | Promise<Record<string, string>>;
|
|
37
|
-
|
|
38
|
-
/** Field-level validator. Returns error message or empty string. */
|
|
39
|
-
export type FieldValidator = (value: unknown, fieldName: string) => string | Promise<string>;
|
|
40
|
-
|
|
41
|
-
/** Form configuration. */
|
|
42
|
-
export interface FormConfig<T extends Record<string, unknown>> {
|
|
43
|
-
/** Initial form values. */
|
|
44
|
-
initialValues: T;
|
|
45
|
-
/** Form-level validation function. */
|
|
46
|
-
validate?: FormValidator<T>;
|
|
47
|
-
/** Field-level validators. */
|
|
48
|
-
fieldValidators?: Partial<Record<keyof T, FieldValidator>>;
|
|
49
|
-
/** Called on valid form submission. */
|
|
50
|
-
onSubmit?: (values: T) => void | Promise<void>;
|
|
51
|
-
/** Called when submit fails validation. */
|
|
52
|
-
onError?: (errors: Record<string, string>) => void;
|
|
53
|
-
/** Validate on every change (default: false). */
|
|
54
|
-
validateOnChange?: boolean;
|
|
55
|
-
/** Validate on blur (default: true). */
|
|
56
|
-
validateOnBlur?: boolean;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/** Field registration props (spread onto input components). */
|
|
60
|
-
export interface FieldRegistration {
|
|
61
|
-
value: unknown;
|
|
62
|
-
onChange: (valueOrEvent: unknown) => void;
|
|
63
|
-
onBlur: () => void;
|
|
64
|
-
name: string;
|
|
65
|
-
error?: string;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/** Form handle returned by useForm. */
|
|
69
|
-
export interface FormHandle<T extends Record<string, unknown>> {
|
|
70
|
-
/** Current form values. */
|
|
71
|
-
values: T;
|
|
72
|
-
/** Current validation errors (keyed by field name). */
|
|
73
|
-
errors: Record<string, string>;
|
|
74
|
-
/** Which fields have been touched (blurred). */
|
|
75
|
-
touched: Record<string, boolean>;
|
|
76
|
-
/** Whether any field has been modified from initial values. */
|
|
77
|
-
dirty: boolean;
|
|
78
|
-
/** Which specific fields are dirty. */
|
|
79
|
-
dirtyFields: Record<string, boolean>;
|
|
80
|
-
/** Whether the form passes validation. */
|
|
81
|
-
isValid: boolean;
|
|
82
|
-
/** Whether the form is currently submitting. */
|
|
83
|
-
isSubmitting: boolean;
|
|
84
|
-
/** Number of times the form has been submitted. */
|
|
85
|
-
submitCount: number;
|
|
86
|
-
/** Set a specific field's value. */
|
|
87
|
-
setField: (name: keyof T, value: unknown) => void;
|
|
88
|
-
/** Set a specific field's error. */
|
|
89
|
-
setError: (name: string, message: string) => void;
|
|
90
|
-
/** Clear a field's error. */
|
|
91
|
-
clearError: (name: string) => void;
|
|
92
|
-
/** Set multiple values at once. */
|
|
93
|
-
setValues: (values: Partial<T>) => void;
|
|
94
|
-
/** Reset form to initial values. */
|
|
95
|
-
reset: (newInitial?: T) => void;
|
|
96
|
-
/** Trigger form submission. */
|
|
97
|
-
handleSubmit: () => Promise<void>;
|
|
98
|
-
/** Validate the entire form manually. */
|
|
99
|
-
validateForm: () => Promise<Record<string, string>>;
|
|
100
|
-
/** Register a field (returns props to spread on input). */
|
|
101
|
-
register: (name: keyof T) => FieldRegistration;
|
|
102
|
-
/** Get a specific field's value. */
|
|
103
|
-
getField: (name: keyof T) => unknown;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// =============================================================================
|
|
107
|
-
// Hook
|
|
108
|
-
// =============================================================================
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Form state management with validation and submit handling.
|
|
112
|
-
*
|
|
113
|
-
* @param config - Form configuration with initial values, validators, and submit handler.
|
|
114
|
-
* @returns Form handle with values, errors, and control methods.
|
|
115
|
-
*/
|
|
116
|
-
export function useForm<T extends Record<string, unknown>>(
|
|
117
|
-
config: FormConfig<T>,
|
|
118
|
-
): FormHandle<T> {
|
|
119
|
-
const {
|
|
120
|
-
initialValues,
|
|
121
|
-
validate,
|
|
122
|
-
fieldValidators,
|
|
123
|
-
onSubmit,
|
|
124
|
-
onError,
|
|
125
|
-
validateOnChange = false,
|
|
126
|
-
validateOnBlur = true,
|
|
127
|
-
} = config;
|
|
128
|
-
|
|
129
|
-
const [values, setValuesState] = useState<T>(initialValues);
|
|
130
|
-
const [errors, setErrors] = useState<Record<string, string>>({});
|
|
131
|
-
const [touched, setTouched] = useState<Record<string, boolean>>({});
|
|
132
|
-
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
133
|
-
const [submitCount, setSubmitCount] = useState(0);
|
|
134
|
-
|
|
135
|
-
const initialRef = useRef(initialValues);
|
|
136
|
-
const validateRef = useRef(validate);
|
|
137
|
-
validateRef.current = validate;
|
|
138
|
-
const fieldValidatorsRef = useRef(fieldValidators);
|
|
139
|
-
fieldValidatorsRef.current = fieldValidators;
|
|
140
|
-
const onSubmitRef = useRef(onSubmit);
|
|
141
|
-
onSubmitRef.current = onSubmit;
|
|
142
|
-
const onErrorRef = useRef(onError);
|
|
143
|
-
onErrorRef.current = onError;
|
|
144
|
-
|
|
145
|
-
// Compute dirty fields
|
|
146
|
-
const dirtyFields = useMemo(() => {
|
|
147
|
-
const result: Record<string, boolean> = {};
|
|
148
|
-
for (const key of Object.keys(values)) {
|
|
149
|
-
result[key] = values[key] !== initialRef.current[key as keyof T];
|
|
150
|
-
}
|
|
151
|
-
return result;
|
|
152
|
-
}, [values]);
|
|
153
|
-
|
|
154
|
-
const dirty = useMemo(
|
|
155
|
-
() => Object.values(dirtyFields).some(Boolean),
|
|
156
|
-
[dirtyFields],
|
|
157
|
-
);
|
|
158
|
-
|
|
159
|
-
const isValid = useMemo(
|
|
160
|
-
() => Object.keys(errors).length === 0,
|
|
161
|
-
[errors],
|
|
162
|
-
);
|
|
163
|
-
|
|
164
|
-
// Validate a single field
|
|
165
|
-
const validateField = useCallback(
|
|
166
|
-
async (name: string, value: unknown): Promise<string> => {
|
|
167
|
-
const validator = fieldValidatorsRef.current?.[name as keyof T];
|
|
168
|
-
if (validator) {
|
|
169
|
-
return await validator(value, name);
|
|
170
|
-
}
|
|
171
|
-
return '';
|
|
172
|
-
},
|
|
173
|
-
[],
|
|
174
|
-
);
|
|
175
|
-
|
|
176
|
-
// Validate entire form
|
|
177
|
-
const validateForm = useCallback(async (): Promise<Record<string, string>> => {
|
|
178
|
-
let formErrors: Record<string, string> = {};
|
|
179
|
-
|
|
180
|
-
// Run form-level validator
|
|
181
|
-
if (validateRef.current) {
|
|
182
|
-
formErrors = await validateRef.current(values);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// Run field-level validators
|
|
186
|
-
if (fieldValidatorsRef.current) {
|
|
187
|
-
for (const [name, validator] of Object.entries(fieldValidatorsRef.current)) {
|
|
188
|
-
if (validator && !formErrors[name]) {
|
|
189
|
-
const fieldError = await (validator as FieldValidator)(values[name as keyof T], name);
|
|
190
|
-
if (fieldError) {
|
|
191
|
-
formErrors[name] = fieldError;
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
setErrors(formErrors);
|
|
198
|
-
return formErrors;
|
|
199
|
-
}, [values]);
|
|
200
|
-
|
|
201
|
-
const setField = useCallback(
|
|
202
|
-
(name: keyof T, value: unknown) => {
|
|
203
|
-
setValuesState((prev) => ({ ...prev, [name]: value }));
|
|
204
|
-
|
|
205
|
-
if (validateOnChange) {
|
|
206
|
-
validateField(name as string, value).then((fieldError) => {
|
|
207
|
-
setErrors((prev) => {
|
|
208
|
-
if (fieldError) {
|
|
209
|
-
return { ...prev, [name]: fieldError };
|
|
210
|
-
}
|
|
211
|
-
const next = { ...prev };
|
|
212
|
-
delete next[name as string];
|
|
213
|
-
return next;
|
|
214
|
-
});
|
|
215
|
-
});
|
|
216
|
-
}
|
|
217
|
-
},
|
|
218
|
-
[validateOnChange, validateField],
|
|
219
|
-
);
|
|
220
|
-
|
|
221
|
-
const setError = useCallback((name: string, message: string) => {
|
|
222
|
-
setErrors((prev) => ({ ...prev, [name]: message }));
|
|
223
|
-
}, []);
|
|
224
|
-
|
|
225
|
-
const clearError = useCallback((name: string) => {
|
|
226
|
-
setErrors((prev) => {
|
|
227
|
-
const next = { ...prev };
|
|
228
|
-
delete next[name];
|
|
229
|
-
return next;
|
|
230
|
-
});
|
|
231
|
-
}, []);
|
|
232
|
-
|
|
233
|
-
const setValues = useCallback((partial: Partial<T>) => {
|
|
234
|
-
setValuesState((prev) => ({ ...prev, ...partial }));
|
|
235
|
-
}, []);
|
|
236
|
-
|
|
237
|
-
const reset = useCallback((newInitial?: T) => {
|
|
238
|
-
const resetTo = newInitial ?? initialRef.current;
|
|
239
|
-
if (newInitial) {
|
|
240
|
-
initialRef.current = newInitial;
|
|
241
|
-
}
|
|
242
|
-
setValuesState(resetTo);
|
|
243
|
-
setErrors({});
|
|
244
|
-
setTouched({});
|
|
245
|
-
setIsSubmitting(false);
|
|
246
|
-
}, []);
|
|
247
|
-
|
|
248
|
-
const handleSubmit = useCallback(async () => {
|
|
249
|
-
setSubmitCount((c) => c + 1);
|
|
250
|
-
|
|
251
|
-
// Touch all fields
|
|
252
|
-
const allTouched: Record<string, boolean> = {};
|
|
253
|
-
for (const key of Object.keys(values)) {
|
|
254
|
-
allTouched[key] = true;
|
|
255
|
-
}
|
|
256
|
-
setTouched(allTouched);
|
|
257
|
-
|
|
258
|
-
// Validate
|
|
259
|
-
const formErrors = await validateForm();
|
|
260
|
-
if (Object.keys(formErrors).length > 0) {
|
|
261
|
-
onErrorRef.current?.(formErrors);
|
|
262
|
-
return;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
// Submit
|
|
266
|
-
if (!onSubmitRef.current) return;
|
|
267
|
-
|
|
268
|
-
setIsSubmitting(true);
|
|
269
|
-
try {
|
|
270
|
-
await onSubmitRef.current(values);
|
|
271
|
-
} catch (err) {
|
|
272
|
-
const submitError = err instanceof Error ? err : new Error(String(err));
|
|
273
|
-
setErrors((prev) => ({ ...prev, _form: submitError.message }));
|
|
274
|
-
} finally {
|
|
275
|
-
setIsSubmitting(false);
|
|
276
|
-
}
|
|
277
|
-
}, [values, validateForm]);
|
|
278
|
-
|
|
279
|
-
const register = useCallback(
|
|
280
|
-
(name: keyof T): FieldRegistration => {
|
|
281
|
-
return {
|
|
282
|
-
name: name as string,
|
|
283
|
-
value: values[name],
|
|
284
|
-
onChange: (valueOrEvent: unknown) => {
|
|
285
|
-
// Handle both raw values and React synthetic events
|
|
286
|
-
let newValue: unknown;
|
|
287
|
-
if (
|
|
288
|
-
valueOrEvent &&
|
|
289
|
-
typeof valueOrEvent === 'object' &&
|
|
290
|
-
'target' in valueOrEvent
|
|
291
|
-
) {
|
|
292
|
-
const target = (valueOrEvent as { target: { type?: string; checked?: boolean; value?: unknown } }).target;
|
|
293
|
-
newValue = target.type === 'checkbox' ? target.checked : target.value;
|
|
294
|
-
} else {
|
|
295
|
-
newValue = valueOrEvent;
|
|
296
|
-
}
|
|
297
|
-
setField(name, newValue);
|
|
298
|
-
},
|
|
299
|
-
onBlur: () => {
|
|
300
|
-
setTouched((prev) => ({ ...prev, [name]: true }));
|
|
301
|
-
|
|
302
|
-
if (validateOnBlur) {
|
|
303
|
-
validateField(name as string, values[name]).then((fieldError) => {
|
|
304
|
-
setErrors((prev) => {
|
|
305
|
-
if (fieldError) {
|
|
306
|
-
return { ...prev, [name]: fieldError };
|
|
307
|
-
}
|
|
308
|
-
const next = { ...prev };
|
|
309
|
-
delete next[name as string];
|
|
310
|
-
return next;
|
|
311
|
-
});
|
|
312
|
-
});
|
|
313
|
-
}
|
|
314
|
-
},
|
|
315
|
-
error: errors[name as string],
|
|
316
|
-
};
|
|
317
|
-
},
|
|
318
|
-
[values, errors, setField, validateOnBlur, validateField],
|
|
319
|
-
);
|
|
320
|
-
|
|
321
|
-
const getField = useCallback(
|
|
322
|
-
(name: keyof T): unknown => values[name],
|
|
323
|
-
[values],
|
|
324
|
-
);
|
|
325
|
-
|
|
326
|
-
return useMemo(
|
|
327
|
-
(): FormHandle<T> => ({
|
|
328
|
-
values,
|
|
329
|
-
errors,
|
|
330
|
-
touched,
|
|
331
|
-
dirty,
|
|
332
|
-
dirtyFields,
|
|
333
|
-
isValid,
|
|
334
|
-
isSubmitting,
|
|
335
|
-
submitCount,
|
|
336
|
-
setField,
|
|
337
|
-
setError,
|
|
338
|
-
clearError,
|
|
339
|
-
setValues,
|
|
340
|
-
reset,
|
|
341
|
-
handleSubmit,
|
|
342
|
-
validateForm,
|
|
343
|
-
register,
|
|
344
|
-
getField,
|
|
345
|
-
}),
|
|
346
|
-
[
|
|
347
|
-
values, errors, touched, dirty, dirtyFields, isValid, isSubmitting,
|
|
348
|
-
submitCount, setField, setError, clearError, setValues, reset,
|
|
349
|
-
handleSubmit, validateForm, register, getField,
|
|
350
|
-
],
|
|
351
|
-
);
|
|
352
|
-
}
|