Skip to content

Commit ec75935

Browse files
committed
feat: add primitives form
1 parent 7081e3b commit ec75935

31 files changed

+4185
-0
lines changed
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"name": "skyroc-form",
3+
"type": "module",
4+
"version": "0.0.1",
5+
"description": "",
6+
"author": "",
7+
"license": "ISC",
8+
"keywords": ["form"],
9+
"exports": {
10+
".": {
11+
"import": "./src/index.ts"
12+
}
13+
},
14+
"scripts": {
15+
"dev": "tsx src/test.ts",
16+
"lint": "eslint src/**/*.ts --fix"
17+
},
18+
"dependencies": {
19+
"@radix-ui/react-slot": "1.2.3",
20+
"skyroc-utils": "workspace:*"
21+
},
22+
"devDependencies": {
23+
"skyroc-type-utils": "workspace:*",
24+
"tsx": "4.20.3"
25+
}
26+
}
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
'use client';
2+
/* eslint-disable react-hooks/exhaustive-deps */
3+
4+
import { Slot } from '@radix-ui/react-slot';
5+
import { useEffect, useId, useRef, useState } from 'react';
6+
import type { AllPaths } from 'skyroc-type-utils';
7+
import { capitalize, getEventValue, isEqual, omitUndefined, toArray } from 'skyroc-utils';
8+
9+
import type { InternalFormInstance } from './FieldContext';
10+
import { useFieldContext } from './FieldContext';
11+
import type { InternalFieldProps } from './types/field';
12+
import type { StoreValue } from './types/formStore';
13+
14+
// eslint-disable-next-line prettier/prettier
15+
const Field = <Values=any, T extends AllPaths<Values> = AllPaths<Values>>(props: InternalFieldProps<Values, T>) => {
16+
const {
17+
children,
18+
controlMode = 'uncontrolled',
19+
getValueFromEvent,
20+
initialValue,
21+
name,
22+
normalize,
23+
preserve = true,
24+
rules,
25+
trigger = 'onChange',
26+
unControlledValueChange,
27+
validateTrigger = 'onChange',
28+
valuePropName = 'value',
29+
...rest
30+
} = props;
31+
32+
// eslint-disable-next-line react/hook-use-state
33+
const [_, forceUpdate] = useState({});
34+
35+
const fieldContext = useFieldContext<Values>();
36+
37+
const normalizedChangedRef = useRef(false);
38+
39+
const key = useId();
40+
41+
const cref = useRef<any>(null);
42+
43+
const { getFieldsValue, getFieldValue, getInternalHooks } = fieldContext as InternalFormInstance<Values>;
44+
45+
const { dispatch, getInitialValue, registerField } = getInternalHooks();
46+
47+
const isControlled = controlMode === 'controlled';
48+
49+
const mergedValidateTrigger = validateTrigger || fieldValidateTrigger;
50+
51+
const initiValue = getInitialValue(name) || initialValue;
52+
53+
const validateTriggerList: string[] = toArray(mergedValidateTrigger);
54+
55+
const make =
56+
(evt: string) =>
57+
(..._args: any[]) =>
58+
dispatch({ name, opts: { trigger: evt }, type: 'validateField' });
59+
60+
const restValidateTriggerList = validateTriggerList
61+
.filter(item => item !== trigger)
62+
.reduce(
63+
(acc, item) => {
64+
acc[item] = make(item);
65+
66+
return acc;
67+
},
68+
{} as Record<string, (...args: any[]) => void>
69+
);
70+
71+
const value = getFieldValue(name);
72+
73+
const valueProps = isControlled
74+
? { [valuePropName]: value }
75+
: { [`default${capitalize(valuePropName)}`]: initiValue };
76+
77+
const controlledProps = omitUndefined({
78+
[trigger]: name
79+
? (...args: any[]) => {
80+
let newValue: StoreValue;
81+
82+
const oldValue = getFieldValue(name);
83+
84+
if (getValueFromEvent) {
85+
newValue = getValueFromEvent(...args);
86+
} else {
87+
newValue = getEventValue(valuePropName, ...args);
88+
}
89+
90+
if (normalize) {
91+
const norm = normalize(newValue, oldValue, getFieldsValue());
92+
93+
if (!isEqual(norm, newValue)) {
94+
newValue = norm;
95+
normalizedChangedRef.current = true;
96+
}
97+
}
98+
99+
if (newValue !== oldValue) {
100+
dispatch({
101+
name,
102+
type: 'updateValue',
103+
value: newValue
104+
});
105+
}
106+
107+
if (validateTriggerList.includes(trigger)) {
108+
dispatch({
109+
name,
110+
opts: { trigger },
111+
type: 'validateField'
112+
});
113+
}
114+
}
115+
: undefined
116+
});
117+
118+
useEffect(() => {
119+
const unregister = registerField({
120+
changeValue: (newValue, isShouldUpdate) => {
121+
if (!isShouldUpdate || !normalizedChangedRef.current) return;
122+
123+
normalizedChangedRef.current = false; // 消费掉标记
124+
125+
if (isControlled) {
126+
forceUpdate({});
127+
return;
128+
}
129+
const el = cref.current;
130+
131+
if (!el) return;
132+
133+
if (!isControlled) {
134+
if (cref.current && !isControlled) {
135+
if (unControlledValueChange) {
136+
unControlledValueChange(cref.current, newValue);
137+
} else {
138+
cref.current.value = newValue as any;
139+
}
140+
}
141+
}
142+
},
143+
initialValue,
144+
name,
145+
preserve
146+
});
147+
148+
dispatch({ name, rules, type: 'setRules' });
149+
150+
return () => {
151+
unregister();
152+
};
153+
}, []);
154+
155+
return (
156+
<Slot
157+
key={key}
158+
{...(valueProps as any)}
159+
{...controlledProps}
160+
{...restValidateTriggerList}
161+
{...rest}
162+
ref={cref}
163+
>
164+
{children}
165+
</Slot>
166+
);
167+
};
168+
169+
export default Field;
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
'use client';
2+
/* eslint-disable no-bitwise */
3+
4+
import { createContext, useContext, useEffect, useRef, useState } from 'react';
5+
import { flushSync } from 'react-dom';
6+
import type { AllPaths, PathToDeepType, ShapeFromPaths } from 'skyroc-type-utils';
7+
8+
import type { ChangeMask, SubscribeMaskOptions } from './form-core/event';
9+
import { toMask } from './form-core/event';
10+
import type { Action, Middleware } from './form-core/middleware';
11+
import type { FieldEntity } from './form-core/types';
12+
import type { ValidateMessages } from './form-core/validate';
13+
import type { Rule } from './form-core/validation';
14+
import type { FormState } from './types';
15+
import type { Meta } from './types/shared-types';
16+
17+
export interface ValuesOptions<Values = any> {
18+
getFieldsValue: <K extends AllPaths<Values, number>[]>(...name: K) => ShapeFromPaths<Values, K>;
19+
getFieldValue: <T extends AllPaths<Values>>(name: T) => PathToDeepType<Values, T>;
20+
setFieldsValue: (values: Partial<Values>) => void;
21+
setFieldValue: <T extends AllPaths<Values>>(name: T, value: PathToDeepType<Values, T>) => void;
22+
}
23+
24+
export interface StateOptions<Values = any> {
25+
getField: <T extends AllPaths<Values>>(name: T) => Meta<T, PathToDeepType<Values, T>>;
26+
getFieldError: (name: AllPaths<Values>) => string[];
27+
getFieldsError: (...name: AllPaths<Values>[]) => Record<AllPaths<Values>, string[]>;
28+
getFieldsWarning: (...name: AllPaths<Values>[]) => Record<AllPaths<Values>, string[]>;
29+
getFieldWarning: (name: AllPaths<Values>) => string[];
30+
getFormState: () => FormState;
31+
isFieldsTouched: (...name: AllPaths<Values>[]) => boolean;
32+
isFieldsValidating: (...name: AllPaths<Values>[]) => boolean;
33+
isFieldTouched: (name: AllPaths<Values>) => boolean;
34+
isFieldValidating: (name: AllPaths<Values>) => boolean;
35+
subscribeField: <T extends AllPaths<Values>>(
36+
name: T,
37+
cb: (value: PathToDeepType<Values, T>, key: T, all: Values, fired: ChangeMask) => void,
38+
opt?: { includeChildren?: boolean; mask?: ChangeMask }
39+
) => () => void;
40+
}
41+
42+
export interface OperationOptions<Values = any> {
43+
resetFields: (...names: AllPaths<Values>[]) => void;
44+
submit: () => void;
45+
use: (mw: Middleware) => void;
46+
validateField: (name: AllPaths<Values>) => Promise<boolean>;
47+
validateFields: (...names: AllPaths<Values>[]) => Promise<boolean>;
48+
}
49+
50+
export interface RegisterCallbackOptions<Values = any> {
51+
onFieldsChange?: (
52+
changedFields: Meta<AllPaths<Values>, PathToDeepType<Values, AllPaths<Values>>>[],
53+
allFields: Meta<AllPaths<Values>, PathToDeepType<Values, AllPaths<Values>>>[]
54+
) => void;
55+
56+
onFinish?: (values: Values) => void;
57+
58+
onValuesChange?: (changedValues: Partial<Values>, values: Values) => void;
59+
}
60+
61+
export interface InternalCallbacks<Values = any> {
62+
destroyForm: (clearOnDestroy?: boolean) => void;
63+
setCallbacks: (callbacks: RegisterCallbackOptions<Values>) => void;
64+
setInitialValues: (values: Partial<Values>) => void;
65+
setPreserve: (preserve: boolean) => void;
66+
setValidateMessages: (messages: ValidateMessages) => void;
67+
}
68+
69+
export interface InternalFieldHooks<Values = any> {
70+
dispatch: (action: Action) => void;
71+
getInitialValue: (name: AllPaths<Values>) => PathToDeepType<Values, AllPaths<Values>>;
72+
registerField: (entity: FieldEntity) => () => void;
73+
setFieldRules: (name: AllPaths<Values>, rules?: Rule[]) => void;
74+
}
75+
76+
export interface FormInstance<Values = any>
77+
extends ValuesOptions<Values>,
78+
StateOptions<Values>,
79+
OperationOptions<Values> {}
80+
81+
export interface InternalFormHooks<Values = any> extends InternalCallbacks<Values>, InternalFieldHooks<Values> {}
82+
83+
export interface InternalFormInstance<Values = any> extends FormInstance<Values> {
84+
/** 内部 API,不建议外部使用 */
85+
getInternalHooks: () => InternalFormHooks<Values>;
86+
}
87+
88+
export const FieldContext = createContext<FormInstance | null>(null);
89+
90+
export const FieldContextProvider = FieldContext.Provider;
91+
92+
export const useFieldContext = <Values = any>(): FormInstance<Values> => {
93+
const context = useContext(FieldContext);
94+
95+
if (!context) {
96+
throw new Error('Can not find FormContext. Please make sure you wrap Field under Form.');
97+
}
98+
99+
return context;
100+
};
101+
102+
export const useFieldState = <Values = any>(
103+
name: AllPaths<Values>,
104+
mask: SubscribeMaskOptions = {
105+
errors: true,
106+
validated: true,
107+
validating: true,
108+
warnings: true
109+
}
110+
) => {
111+
const context = useFieldContext<Values>();
112+
113+
const state = useRef(context.getField(name));
114+
115+
// eslint-disable-next-line react/hook-use-state
116+
const [_, forceUpdate] = useState(0);
117+
118+
if (!context) {
119+
throw new Error('Can not find FormContext. Please make sure you wrap Field under Form.');
120+
}
121+
122+
console.log('context', toMask(mask));
123+
124+
useEffect(() => {
125+
const unregister = context.subscribeField(
126+
name,
127+
() => {
128+
flushSync(() => {
129+
state.current = context.getField(name);
130+
forceUpdate(prev => prev + 1);
131+
});
132+
},
133+
{
134+
mask: toMask(mask)
135+
}
136+
);
137+
138+
return () => {
139+
unregister();
140+
};
141+
}, [context, name]);
142+
143+
return state.current;
144+
};
145+
146+
export const useFieldErrors = <Values = any>(name: AllPaths<Values>) => {
147+
const state = useFieldState<Values>(name, { errors: true });
148+
149+
console.log('state', state);
150+
151+
return state.errors;
152+
};

0 commit comments

Comments
 (0)