@xbeeant/form-engine-react 0.0.3 → 0.0.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/dist/cjs/components/FormController.cjs +1 -1
- package/dist/cjs/components/NexusField.cjs +1 -1
- package/dist/cjs/components/NexusForm.cjs +1 -1
- package/dist/cjs/components/form-controller.cjs +1 -0
- package/dist/cjs/components/nexus-field.cjs +1 -0
- package/dist/cjs/components/nexus-form-provider.cjs +1 -0
- package/dist/cjs/components/nexus-form.cjs +1 -0
- package/dist/cjs/components/nexus-layout.cjs +1 -0
- package/dist/cjs/components/nexus-object.cjs +1 -0
- package/dist/cjs/contexts/field-inherit-context.cjs +1 -0
- package/dist/cjs/contexts/grid-context.cjs +1 -0
- package/dist/cjs/contexts/layout-config-context.cjs +1 -0
- package/dist/cjs/contexts/nexus-context.cjs +1 -0
- package/dist/cjs/hooks/index.cjs +1 -1
- package/dist/cjs/hooks/use-engine.cjs +1 -0
- package/dist/cjs/hooks/use-field-state.cjs +1 -0
- package/dist/cjs/hooks/use-field-validator.cjs +1 -0
- package/dist/cjs/hooks/use-field-value.cjs +1 -0
- package/dist/cjs/hooks/use-form-config.cjs +1 -0
- package/dist/cjs/hooks/use-form-data.cjs +1 -0
- package/dist/cjs/hooks/use-form-submitting.cjs +1 -0
- package/dist/cjs/hooks/use-form.cjs +1 -0
- package/dist/cjs/hooks/use-nexus-context.cjs +1 -0
- package/dist/cjs/hooks/use-watch-all.cjs +1 -0
- package/dist/cjs/hooks/use-watch-multiple.cjs +1 -0
- package/dist/cjs/hooks/use-watch-state.cjs +1 -0
- package/dist/cjs/hooks/use-watch.cjs +1 -0
- package/dist/cjs/hooks/useFormData.cjs +1 -1
- package/dist/cjs/hooks/useFormSubmitting.cjs +1 -0
- package/dist/cjs/index.cjs +1 -1
- package/dist/cjs/styles.css +1 -1
- package/dist/cjs/utils/omit-nil.cjs +1 -0
- package/dist/cjs/utils/omitNil.cjs +1 -0
- package/dist/cjs/utils/persist.cjs +1 -0
- package/dist/cjs/utils/render-tree-node.cjs +1 -0
- package/dist/cjs/utils/resolve-col-span.cjs +1 -0
- package/dist/es/components/FormController.d.ts +73 -19
- package/dist/es/components/FormController.js +222 -71
- package/dist/es/components/NexusField.js +25 -21
- package/dist/es/components/NexusForm.d.ts +33 -3
- package/dist/es/components/NexusForm.js +143 -96
- package/dist/es/components/form-controller.d.ts +129 -0
- package/dist/es/components/form-controller.js +310 -0
- package/dist/es/components/nexus-field.d.ts +9 -0
- package/dist/es/components/nexus-field.js +131 -0
- package/dist/es/components/nexus-form-provider.d.ts +12 -0
- package/dist/es/components/nexus-form-provider.js +18 -0
- package/dist/es/components/nexus-form.d.ts +114 -0
- package/dist/es/components/nexus-form.js +174 -0
- package/dist/es/components/nexus-layout.d.ts +9 -0
- package/dist/es/components/nexus-layout.js +48 -0
- package/dist/es/components/nexus-object.d.ts +16 -0
- package/dist/es/components/nexus-object.js +64 -0
- package/dist/es/contexts/NexusContext.d.ts +2 -0
- package/dist/es/contexts/field-inherit-context.d.ts +18 -0
- package/dist/es/contexts/field-inherit-context.js +5 -0
- package/dist/es/contexts/grid-context.d.ts +5 -0
- package/dist/es/contexts/grid-context.js +5 -0
- package/dist/es/contexts/layout-config-context.d.ts +4 -0
- package/dist/es/contexts/layout-config-context.js +5 -0
- package/dist/es/contexts/nexus-context.d.ts +29 -0
- package/dist/es/contexts/nexus-context.js +12 -0
- package/dist/es/hooks/index.d.ts +12 -11
- package/dist/es/hooks/index.js +20 -18
- package/dist/es/hooks/use-engine.d.ts +2 -0
- package/dist/es/hooks/use-engine.js +8 -0
- package/dist/es/hooks/use-field-state.d.ts +7 -0
- package/dist/es/hooks/use-field-state.js +13 -0
- package/dist/es/hooks/use-field-validator.d.ts +40 -0
- package/dist/es/hooks/use-field-validator.js +35 -0
- package/dist/es/hooks/use-field-value.d.ts +6 -0
- package/dist/es/hooks/use-field-value.js +13 -0
- package/dist/es/hooks/use-form-config.d.ts +5 -0
- package/dist/es/hooks/use-form-config.js +8 -0
- package/dist/es/hooks/use-form-data.d.ts +6 -0
- package/dist/es/hooks/use-form-data.js +13 -0
- package/dist/es/hooks/use-form-submitting.d.ts +7 -0
- package/dist/es/hooks/use-form-submitting.js +11 -0
- package/dist/es/hooks/use-form.d.ts +22 -0
- package/dist/es/hooks/use-form.js +12 -0
- package/dist/es/hooks/use-nexus-context.d.ts +11 -0
- package/dist/es/hooks/use-nexus-context.js +12 -0
- package/dist/es/hooks/use-watch-all.d.ts +19 -0
- package/dist/es/hooks/use-watch-all.js +29 -0
- package/dist/es/hooks/use-watch-multiple.d.ts +20 -0
- package/dist/es/hooks/use-watch-multiple.js +29 -0
- package/dist/es/hooks/use-watch-state.d.ts +18 -0
- package/dist/es/hooks/use-watch-state.js +19 -0
- package/dist/es/hooks/use-watch.d.ts +25 -0
- package/dist/es/hooks/use-watch.js +20 -0
- package/dist/es/hooks/useForm.d.ts +11 -4
- package/dist/es/hooks/useFormData.js +9 -9
- package/dist/es/hooks/useFormSubmitting.d.ts +7 -0
- package/dist/es/hooks/useFormSubmitting.js +11 -0
- package/dist/es/index.d.ts +26 -24
- package/dist/es/index.js +36 -32
- package/dist/es/styles.css +1 -1
- package/dist/es/utils/omit-nil.d.ts +9 -0
- package/dist/es/utils/omit-nil.js +14 -0
- package/dist/es/utils/omitNil.d.ts +9 -0
- package/dist/es/utils/omitNil.js +14 -0
- package/dist/es/utils/persist.d.ts +24 -0
- package/dist/es/utils/persist.js +43 -0
- package/dist/es/utils/render-tree-node.d.ts +6 -0
- package/dist/es/utils/render-tree-node.js +17 -0
- package/dist/es/utils/resolve-col-span.d.ts +5 -0
- package/dist/es/utils/resolve-col-span.js +9 -0
- package/dist/umd/index.css +1 -1
- package/dist/umd/index.umd.cjs +1 -1
- package/package.json +5 -4
- package/src/components/form-controller.ts +584 -0
- package/src/components/{NexusField.tsx → nexus-field.tsx} +57 -6
- package/src/components/{NexusFormProvider.tsx → nexus-form-provider.tsx} +3 -3
- package/src/components/{NexusForm.tsx → nexus-form.tsx} +135 -18
- package/src/components/{NexusLayout.tsx → nexus-layout.tsx} +5 -5
- package/src/components/{NexusObject.tsx → nexus-object.tsx} +3 -3
- package/src/contexts/{NexusContext.ts → nexus-context.ts} +3 -1
- package/src/hooks/index.ts +12 -11
- package/src/hooks/{useEngine.ts → use-engine.ts} +1 -1
- package/src/hooks/{useFieldState.ts → use-field-state.ts} +1 -1
- package/src/hooks/{useFieldValidator.ts → use-field-validator.ts} +1 -1
- package/src/hooks/{useFieldValue.ts → use-field-value.ts} +1 -1
- package/src/hooks/{useFormConfig.ts → use-form-config.ts} +2 -2
- package/src/hooks/{useFormData.ts → use-form-data.ts} +4 -4
- package/src/hooks/use-form-submitting.ts +16 -0
- package/src/hooks/{useForm.ts → use-form.ts} +12 -5
- package/src/hooks/{useNexusContext.ts → use-nexus-context.ts} +2 -2
- package/src/index.ts +26 -24
- package/src/utils/omit-nil.ts +24 -0
- package/src/utils/persist.ts +88 -0
- package/src/utils/{renderTreeNode.tsx → render-tree-node.tsx} +3 -3
- package/src/utils/{resolveColSpan.ts → resolve-col-span.ts} +1 -1
- package/dist/cjs/tailwind.css +0 -1
- package/dist/es/tailwind.css +0 -1
- package/src/components/FormController.ts +0 -278
- /package/src/contexts/{FieldInheritContext.ts → field-inherit-context.ts} +0 -0
- /package/src/contexts/{GridContext.ts → grid-context.ts} +0 -0
- /package/src/contexts/{LayoutConfigContext.ts → layout-config-context.ts} +0 -0
- /package/src/hooks/{useWatchAll.ts → use-watch-all.ts} +0 -0
- /package/src/hooks/{useWatchMultiple.ts → use-watch-multiple.ts} +0 -0
- /package/src/hooks/{useWatchState.ts → use-watch-state.ts} +0 -0
- /package/src/hooks/{useWatch.ts → use-watch.ts} +0 -0
|
@@ -1,16 +1,23 @@
|
|
|
1
|
-
import type { NexusSchema } from '@xbeeant/form-engine';
|
|
1
|
+
import type { NexusComponent, NexusSchema } from '@xbeeant/form-engine';
|
|
2
2
|
import type { CSSProperties, ReactNode, SubmitEvent } from 'react';
|
|
3
3
|
import {
|
|
4
4
|
useCallback,
|
|
5
5
|
useEffect,
|
|
6
|
+
useId,
|
|
6
7
|
useMemo,
|
|
7
8
|
useRef,
|
|
8
9
|
useSyncExternalStore,
|
|
9
10
|
} from 'react';
|
|
10
|
-
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
import { useFormSubmitting } from '../hooks';
|
|
12
|
+
import {
|
|
13
|
+
clearPersisted,
|
|
14
|
+
loadPersisted,
|
|
15
|
+
type PersistOptions,
|
|
16
|
+
savePersisted,
|
|
17
|
+
} from '../utils/persist';
|
|
18
|
+
import { renderTreeNode } from '../utils/render-tree-node';
|
|
19
|
+
import type { FormController } from './form-controller';
|
|
20
|
+
import { NexusFormProvider } from './nexus-form-provider';
|
|
14
21
|
|
|
15
22
|
// ────────────────────────────────────────────────────────────────────────────
|
|
16
23
|
// Form 布局配置
|
|
@@ -31,10 +38,16 @@ export interface NexusFormConfig {
|
|
|
31
38
|
readOnly?: boolean;
|
|
32
39
|
/** 表单每行显示多少列 */
|
|
33
40
|
column?: number;
|
|
41
|
+
/** 表单语言标识(如 'zh-CN' / 'en-US',ui 层消费:antd locale + 内置文案) */
|
|
42
|
+
locale?: string;
|
|
34
43
|
}
|
|
35
44
|
|
|
36
45
|
export interface NexusFormProps {
|
|
37
|
-
/**
|
|
46
|
+
/**
|
|
47
|
+
* Form 实例,由 useForm() 创建。同一 form 引用 = 同一引擎宿主(组件/插件注册共享);
|
|
48
|
+
* 每个 NexusForm 挂载自动获得独立实例(schema/值/订阅互不影响),
|
|
49
|
+
* form 的 API(getValues/setValues/submit...)聚合作用于全部实例。
|
|
50
|
+
*/
|
|
38
51
|
form: FormController;
|
|
39
52
|
/** Schema 定义 */
|
|
40
53
|
schema?: NexusSchema;
|
|
@@ -44,6 +57,8 @@ export interface NexusFormProps {
|
|
|
44
57
|
widgets?: Record<string, (props: any) => ReactNode>;
|
|
45
58
|
/** 额外注册的 layout */
|
|
46
59
|
layouts?: Record<string, (props: any) => ReactNode>;
|
|
60
|
+
/** 字段包裹组件(UI 无关,Renderer 层注入;渲染层默认包裹所有 widget,label=false 时跳过) */
|
|
61
|
+
fieldWrapper?: NexusComponent;
|
|
47
62
|
/** 提交成功回调 */
|
|
48
63
|
onFinish?: (formData: Record<string, unknown>) => void | Promise<void>;
|
|
49
64
|
/** 校验失败回调 */
|
|
@@ -77,12 +92,37 @@ export interface NexusFormProps {
|
|
|
77
92
|
changedPath?: string,
|
|
78
93
|
) => void;
|
|
79
94
|
};
|
|
95
|
+
/**
|
|
96
|
+
* 表单值变化回调(x-render onValuesChange 对齐)
|
|
97
|
+
* 任意字段值变化时触发,参数为 (changedValue, allValues, changedPath)
|
|
98
|
+
* 与 watch: { '#': fn } 语义等价,但作为标准回调 API 提供
|
|
99
|
+
*/
|
|
100
|
+
onValuesChange?: (
|
|
101
|
+
changedValue: unknown,
|
|
102
|
+
allValues: Record<string, unknown>,
|
|
103
|
+
changedPath: string,
|
|
104
|
+
) => void;
|
|
80
105
|
/**
|
|
81
106
|
* 提交时是否移除 hidden 字段数据,默认 true
|
|
82
107
|
* - true: submit/getValues 不包含 hidden 字段
|
|
83
108
|
* - false: submit/getValues 包含所有字段(含 hidden)
|
|
84
109
|
*/
|
|
85
110
|
removeHiddenData?: boolean;
|
|
111
|
+
/**
|
|
112
|
+
* 提交时递归移除空值(undefined/null/'',ProForm omitNil 对齐)
|
|
113
|
+
* 与 form.submit({ omitNil: true }) / form.getValues(paths, { omitNil: true }) 等价
|
|
114
|
+
*/
|
|
115
|
+
omitNil?: boolean;
|
|
116
|
+
/** 表单语言标识(如 'zh-CN' / 'en-US',ui 层消费:antd locale + 内置文案) */
|
|
117
|
+
locale?: string;
|
|
118
|
+
/**
|
|
119
|
+
* 表单草稿持久化:值变化时自动保存到 Web Storage,下次挂载自动恢复
|
|
120
|
+
* @example
|
|
121
|
+
* ```tsx
|
|
122
|
+
* <NexusForm persist={{ key: 'apply-form', storage: 'localStorage' }} />
|
|
123
|
+
* ```
|
|
124
|
+
*/
|
|
125
|
+
persist?: PersistOptions;
|
|
86
126
|
|
|
87
127
|
// ── 表单布局配置 ──────────────────────────────────────────────────────
|
|
88
128
|
/** label 列配置(由 ui 层 Form.Item 消费) */
|
|
@@ -112,6 +152,7 @@ export function NexusForm({
|
|
|
112
152
|
initialValues,
|
|
113
153
|
widgets,
|
|
114
154
|
layouts,
|
|
155
|
+
fieldWrapper,
|
|
115
156
|
onFinish,
|
|
116
157
|
onFinishFailed,
|
|
117
158
|
onMount,
|
|
@@ -127,10 +168,24 @@ export function NexusForm({
|
|
|
127
168
|
readOnly,
|
|
128
169
|
column,
|
|
129
170
|
watch,
|
|
171
|
+
onValuesChange,
|
|
130
172
|
removeHiddenData = true,
|
|
173
|
+
omitNil = false,
|
|
174
|
+
locale,
|
|
175
|
+
persist,
|
|
131
176
|
}: NexusFormProps) {
|
|
132
|
-
|
|
177
|
+
// 实例视图引擎:同一 form 挂载的每个 NexusForm 自动获得独立实例
|
|
178
|
+
// (schema/值/订阅互不影响);实例标识由 useId 内部生成,用户不感知 instanceId。
|
|
179
|
+
// 同一 form = 同一引擎宿主(组件/插件注册共享),不同 schema = 不同实例状态。
|
|
180
|
+
const instanceKey = useId();
|
|
181
|
+
const engine = useMemo(
|
|
182
|
+
() => form._useInstance(instanceKey),
|
|
183
|
+
[form, instanceKey],
|
|
184
|
+
);
|
|
133
185
|
const formElRef = useRef<HTMLFormElement | null>(null);
|
|
186
|
+
// persist 配置经 ref 持有:保存回调不随配置对象变化重建订阅
|
|
187
|
+
const persistRef = useRef<PersistOptions | undefined>(persist);
|
|
188
|
+
persistRef.current = persist;
|
|
134
189
|
|
|
135
190
|
// Schema 顶层配置作为默认值,props 优先级更高
|
|
136
191
|
const finalDisplayType = displayType ?? schema?.displayType ?? 'row';
|
|
@@ -139,16 +194,20 @@ export function NexusForm({
|
|
|
139
194
|
const finalLabelWidth = labelWidth ?? schema?.labelWidth;
|
|
140
195
|
const finalReadOnly = readOnly ?? schema?.readOnly ?? false;
|
|
141
196
|
const finalColumn = column ?? schema?.column;
|
|
197
|
+
const finalLocale = locale ?? schema?.locale ?? engine.getLocale();
|
|
142
198
|
|
|
143
199
|
// 注册额外 widgets / layouts(仅首次或引用变化时)
|
|
144
200
|
useEffect(() => {
|
|
145
201
|
if (widgets) {
|
|
146
202
|
engine.registerWidgets(widgets);
|
|
147
203
|
}
|
|
204
|
+
if (fieldWrapper) {
|
|
205
|
+
engine.registerFieldWrapper(fieldWrapper);
|
|
206
|
+
}
|
|
148
207
|
if (layouts) {
|
|
149
208
|
engine.registerLayouts(layouts);
|
|
150
209
|
}
|
|
151
|
-
}, [engine, widgets, layouts]);
|
|
210
|
+
}, [engine, widgets, layouts, fieldWrapper]);
|
|
152
211
|
|
|
153
212
|
// schema 变化时重新初始化
|
|
154
213
|
// 注意:initialValues 仅在首次挂载时使用,避免每次渲染都 re-init 导致
|
|
@@ -160,7 +219,11 @@ export function NexusForm({
|
|
|
160
219
|
return;
|
|
161
220
|
}
|
|
162
221
|
if (isFirstInitRef.current) {
|
|
163
|
-
|
|
222
|
+
// 草稿持久化:恢复已保存草稿(优先于 initialValues)
|
|
223
|
+
const persisted = persistRef.current
|
|
224
|
+
? loadPersisted(persistRef.current)
|
|
225
|
+
: undefined;
|
|
226
|
+
engine.init(schema, persisted ?? initialValuesRef.current);
|
|
164
227
|
isFirstInitRef.current = false;
|
|
165
228
|
} else {
|
|
166
229
|
// schema 变化:保留当前已填数据,而非重置为 initialValues
|
|
@@ -169,6 +232,35 @@ export function NexusForm({
|
|
|
169
232
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
170
233
|
}, [engine, schema]);
|
|
171
234
|
|
|
235
|
+
// 草稿持久化:store 版本变化(任意数据变更)时防抖保存
|
|
236
|
+
// 用持久化身份(storage:key)驱动订阅:开关切换 / 换 key 时重建订阅,
|
|
237
|
+
// 而 persist 对象本身每次渲染重建不影响订阅稳定性
|
|
238
|
+
const persistIdentity = persist
|
|
239
|
+
? `${persist.storage ?? 'localStorage'}:${persist.key}`
|
|
240
|
+
: null;
|
|
241
|
+
useEffect(() => {
|
|
242
|
+
const options = persistIdentity ? persistRef.current : undefined;
|
|
243
|
+
if (!options) {
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
247
|
+
const debounce = options.debounce ?? 300;
|
|
248
|
+
const unsub = engine.subscribeStore(() => {
|
|
249
|
+
if (timer) {
|
|
250
|
+
clearTimeout(timer);
|
|
251
|
+
}
|
|
252
|
+
timer = setTimeout(() => {
|
|
253
|
+
savePersisted(options, engine.getFormData());
|
|
254
|
+
}, debounce);
|
|
255
|
+
});
|
|
256
|
+
return () => {
|
|
257
|
+
if (timer) {
|
|
258
|
+
clearTimeout(timer);
|
|
259
|
+
}
|
|
260
|
+
unsub();
|
|
261
|
+
};
|
|
262
|
+
}, [engine, persistIdentity]);
|
|
263
|
+
|
|
172
264
|
// onMount:首次传入「非空」schema 并完成渲染后执行一次
|
|
173
265
|
// undefined / null / {}(无任何键)均视为空 schema,不触发;
|
|
174
266
|
// schema 由空变为非空时,于首个非空渲染提交(useEffect)后触发。
|
|
@@ -191,28 +283,48 @@ export function NexusForm({
|
|
|
191
283
|
useRef<(data: Record<string, unknown>) => void | Promise<void>>(noop);
|
|
192
284
|
const onFinishFailedRef =
|
|
193
285
|
useRef<(errors: Map<string, string[]>) => void>(noop);
|
|
194
|
-
onFinishRef.current =
|
|
286
|
+
onFinishRef.current = (data) => {
|
|
287
|
+
// persist 未配置时不参与提交流程;配置且 clearOnSubmit !== false 时清除草稿
|
|
288
|
+
const options = persistRef.current;
|
|
289
|
+
if (options && options.clearOnSubmit !== false) {
|
|
290
|
+
clearPersisted(options);
|
|
291
|
+
}
|
|
292
|
+
return onFinish?.(data);
|
|
293
|
+
};
|
|
195
294
|
onFinishFailedRef.current = onFinishFailed ?? noop;
|
|
196
295
|
|
|
197
296
|
// 只在挂载时绑定一次:传入「稳定的 getter」,让 FormController 在 submit 时读取最新回调
|
|
198
297
|
useEffect(() => {
|
|
199
298
|
form._bind(
|
|
299
|
+
instanceKey,
|
|
200
300
|
formElRef.current,
|
|
201
301
|
() => onFinishRef.current,
|
|
202
302
|
() => onFinishFailedRef.current,
|
|
203
303
|
);
|
|
204
304
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
205
|
-
}, [form]);
|
|
305
|
+
}, [form, instanceKey]);
|
|
206
306
|
|
|
207
|
-
// watch / removeHiddenData 变化时单独同步(不重置其他绑定)
|
|
307
|
+
// watch / removeHiddenData / onValuesChange 变化时单独同步(不重置其他绑定)
|
|
308
|
+
// onValuesChange 经 ref 持有:回调内容变化不重建订阅,仅内部引用更新
|
|
309
|
+
const onValuesChangeRef = useRef(onValuesChange);
|
|
310
|
+
onValuesChangeRef.current = onValuesChange;
|
|
208
311
|
useEffect(() => {
|
|
209
|
-
form._syncConfig(
|
|
210
|
-
|
|
312
|
+
form._syncConfig(instanceKey, {
|
|
313
|
+
removeHiddenData,
|
|
314
|
+
omitNil,
|
|
315
|
+
watch,
|
|
316
|
+
onValuesChange: (changedValue, allValues, changedPath) =>
|
|
317
|
+
onValuesChangeRef.current?.(changedValue, allValues, changedPath),
|
|
318
|
+
});
|
|
319
|
+
}, [form, instanceKey, removeHiddenData, omitNil, watch]);
|
|
211
320
|
|
|
321
|
+
// 渲染树版本订阅:仅 Schema 结构变化(init/setSchema/reset)时重算 renderTree。
|
|
322
|
+
// 字段值/错误等数据变化只 bump store 版本(useFormData 消费),
|
|
323
|
+
// 不会触发 NexusForm 重渲染——各字段经字段级版本订阅精准重渲染。
|
|
212
324
|
const _version = useSyncExternalStore(
|
|
213
|
-
engine.
|
|
214
|
-
engine.
|
|
215
|
-
engine.
|
|
325
|
+
(cb) => engine.subscribeRender(cb),
|
|
326
|
+
() => engine.getRenderSnapshot(),
|
|
327
|
+
() => engine.getRenderSnapshot(),
|
|
216
328
|
);
|
|
217
329
|
// 依赖 _version:engine.init() / setSchema() 会 bump version,
|
|
218
330
|
// 需要在此后重新读取 renderTree(首次渲染时 engine 尚未 init,renderTree 为空)
|
|
@@ -232,11 +344,14 @@ export function NexusForm({
|
|
|
232
344
|
}, [form]);
|
|
233
345
|
|
|
234
346
|
// footer 渲染
|
|
347
|
+
const submitting = useFormSubmitting(form);
|
|
235
348
|
let footerNode: ReactNode = null;
|
|
236
349
|
if (footer === true) {
|
|
237
350
|
footerNode = (
|
|
238
351
|
<div className='mt-4'>
|
|
239
|
-
<button type='submit'
|
|
352
|
+
<button type='submit' disabled={submitting}>
|
|
353
|
+
{submitting ? '提交中...' : '提交'}
|
|
354
|
+
</button>{' '}
|
|
240
355
|
<button type='button' onClick={handleReset}>
|
|
241
356
|
重置
|
|
242
357
|
</button>
|
|
@@ -274,6 +389,7 @@ export function NexusForm({
|
|
|
274
389
|
displayType: finalDisplayType,
|
|
275
390
|
readOnly: finalReadOnly,
|
|
276
391
|
column: finalColumn,
|
|
392
|
+
locale: finalLocale,
|
|
277
393
|
}),
|
|
278
394
|
[
|
|
279
395
|
mergedLabelCol,
|
|
@@ -283,6 +399,7 @@ export function NexusForm({
|
|
|
283
399
|
finalDisplayType,
|
|
284
400
|
finalReadOnly,
|
|
285
401
|
finalColumn,
|
|
402
|
+
finalLocale,
|
|
286
403
|
],
|
|
287
404
|
);
|
|
288
405
|
|
|
@@ -2,14 +2,14 @@ import type { RenderLayoutNode } from '@xbeeant/form-engine';
|
|
|
2
2
|
import type { CSSProperties } from 'react';
|
|
3
3
|
import { useContext, useMemo } from 'react';
|
|
4
4
|
|
|
5
|
-
import { GridContext } from '../contexts/
|
|
5
|
+
import { GridContext } from '../contexts/grid-context';
|
|
6
6
|
import {
|
|
7
7
|
LayoutConfigContext,
|
|
8
8
|
type LayoutConfigContextValue,
|
|
9
|
-
} from '../contexts/
|
|
10
|
-
import { useNexusContext } from '../contexts/
|
|
11
|
-
import { renderTreeNode } from '../utils/
|
|
12
|
-
import { resolveColSpan } from '../utils/
|
|
9
|
+
} from '../contexts/layout-config-context';
|
|
10
|
+
import { useNexusContext } from '../contexts/nexus-context';
|
|
11
|
+
import { renderTreeNode } from '../utils/render-tree-node';
|
|
12
|
+
import { resolveColSpan } from '../utils/resolve-col-span';
|
|
13
13
|
|
|
14
14
|
interface NexusLayoutProps {
|
|
15
15
|
node: RenderLayoutNode;
|
|
@@ -2,9 +2,9 @@ import type { RenderObjectNode } from '@xbeeant/form-engine';
|
|
|
2
2
|
import type { CSSProperties } from 'react';
|
|
3
3
|
import { useContext, useState, useSyncExternalStore } from 'react';
|
|
4
4
|
|
|
5
|
-
import { FieldInheritContext } from '../contexts/
|
|
6
|
-
import { useNexusContext } from '../contexts/
|
|
7
|
-
import { renderTreeNode } from '../utils/
|
|
5
|
+
import { FieldInheritContext } from '../contexts/field-inherit-context';
|
|
6
|
+
import { useNexusContext } from '../contexts/nexus-context';
|
|
7
|
+
import { renderTreeNode } from '../utils/render-tree-node';
|
|
8
8
|
|
|
9
9
|
interface NexusObjectProps {
|
|
10
10
|
node: RenderObjectNode;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { NexusEngine } from '@xbeeant/form-engine';
|
|
2
2
|
import { createContext, type ReactNode, useContext } from 'react';
|
|
3
3
|
|
|
4
|
-
import type { FormController } from '../components/
|
|
4
|
+
import type { FormController } from '../components/form-controller';
|
|
5
5
|
|
|
6
6
|
// ────────────────────────────────────────────────────────────────────────────
|
|
7
7
|
// Form 布局配置
|
|
@@ -22,6 +22,8 @@ export interface NexusFormConfig {
|
|
|
22
22
|
readOnly?: boolean;
|
|
23
23
|
/** 表单每行显示多少列 */
|
|
24
24
|
column?: number;
|
|
25
|
+
/** 表单语言标识(如 'zh-CN' / 'en-US',ui 层消费:antd locale + 内置文案) */
|
|
26
|
+
locale?: string;
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
interface NexusContextValue {
|
package/src/hooks/index.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
export { useEngine } from './
|
|
2
|
-
export { useFieldState } from './
|
|
3
|
-
export { useFieldValidator } from './
|
|
4
|
-
export { useFieldValue } from './
|
|
5
|
-
export { useForm } from './
|
|
6
|
-
export { useFormConfig } from './
|
|
7
|
-
export { useFormData } from './
|
|
8
|
-
export {
|
|
9
|
-
export {
|
|
10
|
-
export {
|
|
11
|
-
export {
|
|
1
|
+
export { useEngine } from './use-engine';
|
|
2
|
+
export { useFieldState } from './use-field-state';
|
|
3
|
+
export { useFieldValidator } from './use-field-validator';
|
|
4
|
+
export { useFieldValue } from './use-field-value';
|
|
5
|
+
export { useForm } from './use-form';
|
|
6
|
+
export { useFormConfig } from './use-form-config';
|
|
7
|
+
export { useFormData } from './use-form-data';
|
|
8
|
+
export { useFormSubmitting } from './use-form-submitting';
|
|
9
|
+
export { useWatch } from './use-watch';
|
|
10
|
+
export { useWatchAll } from './use-watch-all';
|
|
11
|
+
export { useWatchMultiple } from './use-watch-multiple';
|
|
12
|
+
export { useWatchState } from './use-watch-state';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { NexusEngine } from '@xbeeant/form-engine';
|
|
2
2
|
|
|
3
|
-
import { useNexusContext } from '../contexts/
|
|
3
|
+
import { useNexusContext } from '../contexts/nexus-context';
|
|
4
4
|
|
|
5
5
|
export function useEngine(): NexusEngine {
|
|
6
6
|
const { engine } = useNexusContext();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { FieldState } from '@xbeeant/form-engine';
|
|
2
2
|
import { useSyncExternalStore } from 'react';
|
|
3
3
|
|
|
4
|
-
import { useNexusContext } from '../contexts/
|
|
4
|
+
import { useNexusContext } from '../contexts/nexus-context';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* useFieldState — 精准订阅单个字段状态
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
|
|
25
25
|
import type { NexusEngine, NexusFormInstance } from '@xbeeant/form-engine';
|
|
26
26
|
import { useEffect, useRef } from 'react';
|
|
27
|
-
import { useNexusContext } from '../contexts/
|
|
27
|
+
import { useNexusContext } from '../contexts/nexus-context';
|
|
28
28
|
|
|
29
29
|
export type FieldValidator = (
|
|
30
30
|
value: unknown,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { NexusFormConfig } from '../components/
|
|
2
|
-
import { useNexusContext } from '../contexts/
|
|
1
|
+
import type { NexusFormConfig } from '../components/nexus-form';
|
|
2
|
+
import { useNexusContext } from '../contexts/nexus-context';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* useFormConfig — 获取表单布局配置
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useMemo, useSyncExternalStore } from 'react';
|
|
2
2
|
|
|
3
|
-
import { useEngine } from './
|
|
3
|
+
import { useEngine } from './use-engine';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* useFormData — 订阅整个表单数据
|
|
@@ -11,9 +11,9 @@ export function useFormData(): Record<string, unknown> {
|
|
|
11
11
|
const engine = useEngine();
|
|
12
12
|
|
|
13
13
|
const _version = useSyncExternalStore(
|
|
14
|
-
engine.subscribeStore,
|
|
15
|
-
engine.getSnapshot,
|
|
16
|
-
engine.getSnapshot,
|
|
14
|
+
(cb) => engine.subscribeStore(cb),
|
|
15
|
+
() => engine.getSnapshot(),
|
|
16
|
+
() => engine.getSnapshot(),
|
|
17
17
|
);
|
|
18
18
|
// biome-ignore lint/correctness/useExhaustiveDependencies: _version 是 formData 失效信号(engine 内部状态,静态分析不可见)
|
|
19
19
|
return useMemo(() => engine.getFormData(), [engine, _version]);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { useSyncExternalStore } from 'react';
|
|
2
|
+
|
|
3
|
+
import type { FormController } from '../components/form-controller';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* useFormSubmitting — 订阅表单提交中状态(formily submitting 对齐)
|
|
7
|
+
*
|
|
8
|
+
* submit() 全流程(校验 + onFinish)期间返回 true,供提交按钮展示 loading。
|
|
9
|
+
*/
|
|
10
|
+
export function useFormSubmitting(form: FormController): boolean {
|
|
11
|
+
return useSyncExternalStore(
|
|
12
|
+
(cb) => form.onSubmittingChange(cb),
|
|
13
|
+
() => form.getSubmitting(),
|
|
14
|
+
() => form.getSubmitting(),
|
|
15
|
+
);
|
|
16
|
+
}
|
|
@@ -1,19 +1,26 @@
|
|
|
1
1
|
import { NexusEngine } from '@xbeeant/form-engine';
|
|
2
2
|
import { useRef } from 'react';
|
|
3
3
|
|
|
4
|
-
import { FormController } from '../components/
|
|
4
|
+
import { FormController } from '../components/form-controller';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* useForm — 创建 Form 实例
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
* -
|
|
9
|
+
* 一个 Form 绑定一个引擎宿主(引擎为独立实体):
|
|
10
|
+
* - 同一 form 引用传给多个 NexusForm → 共享引擎宿主(组件/插件注册共享),
|
|
11
|
+
* 每个 NexusForm 挂载自动获得独立实例(schema/值/订阅互不影响),
|
|
12
|
+
* form 的 API 聚合作用于全部实例
|
|
13
|
+
* - 需要完全独立的表单:各自 useForm()(独立引擎宿主)
|
|
14
|
+
*
|
|
15
|
+
* 支持:
|
|
16
|
+
* - `useForm()`:创建独立实例(引擎宿主内部创建,未注册,不参与跨表单联动)
|
|
11
17
|
* - `useForm(formId)`:创建带 formId 的实例,自动注册到默认表单注册表,
|
|
12
18
|
* 可通过 schema `crossForm` reaction / `engine.linkForm` 与其他表单联动
|
|
13
|
-
* - `useForm(formId, engine)`:复用外部创建的引擎(如 `new NexusEngine({ formId })
|
|
19
|
+
* - `useForm(formId, engine)`:复用外部创建的引擎(如 `new NexusEngine({ formId })`),
|
|
20
|
+
* 实现多个 form 共享同一引擎宿主
|
|
14
21
|
*
|
|
15
22
|
* @param formId - 可选,表单实例唯一标识(跨表单联动寻址)
|
|
16
|
-
* @param engine -
|
|
23
|
+
* @param engine - 可选,外部引擎宿主(缺省内部创建)
|
|
17
24
|
*/
|
|
18
25
|
export function useForm(
|
|
19
26
|
formId?: string,
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
import type { NexusEngine } from '@xbeeant/form-engine';
|
|
6
6
|
import { createContext, useContext } from 'react';
|
|
7
|
-
import type { FormController } from '../components/
|
|
8
|
-
import type { NexusFormConfig } from '../components/
|
|
7
|
+
import type { FormController } from '../components/form-controller';
|
|
8
|
+
import type { NexusFormConfig } from '../components/nexus-form';
|
|
9
9
|
|
|
10
10
|
interface NexusContextValue {
|
|
11
11
|
engine: NexusEngine;
|
package/src/index.ts
CHANGED
|
@@ -5,28 +5,30 @@
|
|
|
5
5
|
|
|
6
6
|
import './styles.css';
|
|
7
7
|
|
|
8
|
-
export { FormController } from './components/
|
|
9
|
-
export { NexusField } from './components/
|
|
10
|
-
export type { NexusFormConfig } from './components/
|
|
11
|
-
export { NexusForm } from './components/
|
|
12
|
-
export { NexusFormProvider } from './components/
|
|
13
|
-
export { NexusLayout } from './components/
|
|
14
|
-
export { NexusObject } from './components/
|
|
15
|
-
export type { FieldInheritValue } from './contexts/
|
|
16
|
-
export { FieldInheritContext } from './contexts/
|
|
17
|
-
export type { GridContextValue } from './contexts/
|
|
18
|
-
export { GridContext } from './contexts/
|
|
19
|
-
export type { LayoutConfigContextValue } from './contexts/
|
|
20
|
-
export { LayoutConfigContext } from './contexts/
|
|
8
|
+
export { FormController } from './components/form-controller';
|
|
9
|
+
export { NexusField } from './components/nexus-field';
|
|
10
|
+
export type { NexusFormConfig, NexusFormProps } from './components/nexus-form';
|
|
11
|
+
export { NexusForm } from './components/nexus-form';
|
|
12
|
+
export { NexusFormProvider } from './components/nexus-form-provider';
|
|
13
|
+
export { NexusLayout } from './components/nexus-layout';
|
|
14
|
+
export { NexusObject } from './components/nexus-object';
|
|
15
|
+
export type { FieldInheritValue } from './contexts/field-inherit-context';
|
|
16
|
+
export { FieldInheritContext } from './contexts/field-inherit-context';
|
|
17
|
+
export type { GridContextValue } from './contexts/grid-context';
|
|
18
|
+
export { GridContext } from './contexts/grid-context';
|
|
19
|
+
export type { LayoutConfigContextValue } from './contexts/layout-config-context';
|
|
20
|
+
export { LayoutConfigContext } from './contexts/layout-config-context';
|
|
21
|
+
export { NexusContext } from './contexts/nexus-context';
|
|
21
22
|
|
|
22
|
-
export { useEngine } from './hooks/
|
|
23
|
-
export { useFieldState } from './hooks/
|
|
24
|
-
export { useFieldValidator } from './hooks/
|
|
25
|
-
export { useFieldValue } from './hooks/
|
|
26
|
-
export { useForm } from './hooks/
|
|
27
|
-
export { useFormConfig } from './hooks/
|
|
28
|
-
export { useFormData } from './hooks/
|
|
29
|
-
export {
|
|
30
|
-
export {
|
|
31
|
-
export {
|
|
32
|
-
export {
|
|
23
|
+
export { useEngine } from './hooks/use-engine';
|
|
24
|
+
export { useFieldState } from './hooks/use-field-state';
|
|
25
|
+
export { useFieldValidator } from './hooks/use-field-validator';
|
|
26
|
+
export { useFieldValue } from './hooks/use-field-value';
|
|
27
|
+
export { useForm } from './hooks/use-form';
|
|
28
|
+
export { useFormConfig } from './hooks/use-form-config';
|
|
29
|
+
export { useFormData } from './hooks/use-form-data';
|
|
30
|
+
export { useFormSubmitting } from './hooks/use-form-submitting';
|
|
31
|
+
export { useWatch } from './hooks/use-watch';
|
|
32
|
+
export { useWatchAll } from './hooks/use-watch-all';
|
|
33
|
+
export { useWatchMultiple } from './hooks/use-watch-multiple';
|
|
34
|
+
export { useWatchState } from './hooks/use-watch-state';
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* omitNilDeep — 递归移除空值(undefined / null / '')
|
|
3
|
+
*
|
|
4
|
+
* ProForm omitNil 对齐:提交/取值时过滤掉未填写的空值字段。
|
|
5
|
+
* - 对象:移除值为空的键(递归处理嵌套对象)
|
|
6
|
+
* - 数组:逐项递归处理(保留数组结构与长度)
|
|
7
|
+
* - 0 / false / NaN 等非空值保留
|
|
8
|
+
*/
|
|
9
|
+
export function omitNilDeep(value: unknown): unknown {
|
|
10
|
+
if (Array.isArray(value)) {
|
|
11
|
+
return value.map((item) => omitNilDeep(item));
|
|
12
|
+
}
|
|
13
|
+
if (value !== null && typeof value === 'object') {
|
|
14
|
+
const result: Record<string, unknown> = {};
|
|
15
|
+
for (const [key, item] of Object.entries(value)) {
|
|
16
|
+
if (item === undefined || item === null || item === '') {
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
result[key] = omitNilDeep(item);
|
|
20
|
+
}
|
|
21
|
+
return result;
|
|
22
|
+
}
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 表单草稿持久化(NexusForm persist 选项实现)
|
|
3
|
+
*
|
|
4
|
+
* 将表单数据自动保存到 Web Storage(localStorage / sessionStorage),
|
|
5
|
+
* 下次挂载时自动恢复为初始值(草稿续填)。
|
|
6
|
+
*
|
|
7
|
+
* 约束:
|
|
8
|
+
* - 所有存储访问均 try/catch 包裹(SSR 无 window / Safari 隐私模式抛错)
|
|
9
|
+
* - 存储键为完整路径前缀,避免多表单实例冲突
|
|
10
|
+
* - 提交成功后默认清除草稿(clearOnSubmit: false 可关闭)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export interface PersistOptions {
|
|
14
|
+
/** 存储键(必填,多表单实例需唯一) */
|
|
15
|
+
key: string;
|
|
16
|
+
/** 存储介质,默认 localStorage */
|
|
17
|
+
storage?: 'localStorage' | 'sessionStorage';
|
|
18
|
+
/** 保存防抖毫秒数,默认 300 */
|
|
19
|
+
debounce?: number;
|
|
20
|
+
/** 提交成功后清除草稿,默认 true */
|
|
21
|
+
clearOnSubmit?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function getStorage(
|
|
25
|
+
storage?: 'localStorage' | 'sessionStorage',
|
|
26
|
+
): Storage | undefined {
|
|
27
|
+
if (typeof window === 'undefined') {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
const target =
|
|
32
|
+
storage === 'sessionStorage'
|
|
33
|
+
? window.sessionStorage
|
|
34
|
+
: window.localStorage;
|
|
35
|
+
// 隐私模式下访问可能抛错,统一降级为不持久化
|
|
36
|
+
target.setItem('__nexus_persist_probe__', '1');
|
|
37
|
+
target.removeItem('__nexus_persist_probe__');
|
|
38
|
+
return target;
|
|
39
|
+
} catch {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function loadPersisted(
|
|
45
|
+
options: PersistOptions,
|
|
46
|
+
): Record<string, unknown> | undefined {
|
|
47
|
+
const storage = getStorage(options.storage);
|
|
48
|
+
if (!storage) {
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
const raw = storage.getItem(options.key);
|
|
53
|
+
if (!raw) {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
const parsed = JSON.parse(raw) as Record<string, unknown>;
|
|
57
|
+
return typeof parsed === 'object' && parsed !== null ? parsed : undefined;
|
|
58
|
+
} catch {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function savePersisted(
|
|
64
|
+
options: PersistOptions,
|
|
65
|
+
data: Record<string, unknown>,
|
|
66
|
+
): void {
|
|
67
|
+
const storage = getStorage(options.storage);
|
|
68
|
+
if (!storage) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
storage.setItem(options.key, JSON.stringify(data));
|
|
73
|
+
} catch {
|
|
74
|
+
// 存储配额满 / 隐私模式:静默失败
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function clearPersisted(options: PersistOptions): void {
|
|
79
|
+
const storage = getStorage(options.storage);
|
|
80
|
+
if (!storage) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
storage.removeItem(options.key);
|
|
85
|
+
} catch {
|
|
86
|
+
// 静默失败
|
|
87
|
+
}
|
|
88
|
+
}
|