@xbeeant/form-engine-react 0.0.2 → 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/useForm.cjs +1 -1
- 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 -0
- 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 +76 -18
- package/dist/es/components/FormController.js +227 -70
- package/dist/es/components/NexusField.js +63 -60
- package/dist/es/components/NexusForm.d.ts +40 -3
- package/dist/es/components/NexusForm.js +143 -89
- 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 +17 -1
- package/dist/es/hooks/useForm.js +9 -6
- 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 +43 -38
- package/dist/es/styles.css +1 -0
- 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 -0
- 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} +108 -31
- package/src/components/{NexusFormProvider.tsx → nexus-form-provider.tsx} +3 -3
- package/src/components/{NexusForm.tsx → nexus-form.tsx} +159 -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/use-form.ts +36 -0
- package/src/hooks/{useNexusContext.ts → use-nexus-context.ts} +2 -2
- package/src/index.ts +28 -24
- package/src/styles.css +6 -0
- 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/src/vite-env.d.ts +1 -0
- package/src/components/FormController.ts +0 -271
- package/src/hooks/useForm.ts +0 -15
- /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,10 +1,10 @@
|
|
|
1
|
-
import type { CSSProperties, FocusEvent } from 'react';
|
|
1
|
+
import type { CSSProperties, FocusEvent, ReactElement } from 'react';
|
|
2
2
|
import { useCallback, useContext, useMemo, useSyncExternalStore } from 'react';
|
|
3
|
-
import { FieldInheritContext } from '../contexts/
|
|
4
|
-
import { GridContext } from '../contexts/
|
|
5
|
-
import { LayoutConfigContext } from '../contexts/
|
|
6
|
-
import { useNexusContext } from '../contexts/
|
|
7
|
-
import { resolveColSpan } from '../utils/
|
|
3
|
+
import { FieldInheritContext } from '../contexts/field-inherit-context';
|
|
4
|
+
import { GridContext } from '../contexts/grid-context';
|
|
5
|
+
import { LayoutConfigContext } from '../contexts/layout-config-context';
|
|
6
|
+
import { useNexusContext } from '../contexts/nexus-context';
|
|
7
|
+
import { resolveColSpan } from '../utils/resolve-col-span';
|
|
8
8
|
|
|
9
9
|
interface NexusFieldProps {
|
|
10
10
|
dataPath: string;
|
|
@@ -82,6 +82,49 @@ export function NexusField({ dataPath, layoutKey }: NexusFieldProps) {
|
|
|
82
82
|
return values;
|
|
83
83
|
}, [state?.reactions, engine]);
|
|
84
84
|
|
|
85
|
+
// x-render addons — 为 widget 组件提供统一的表单数据访问、校验、Schema 操作入口
|
|
86
|
+
// 对齐 x-render 自定义组件的 addons API
|
|
87
|
+
const addons = useMemo(() => {
|
|
88
|
+
const arrayPath = state?.meta.itemOf;
|
|
89
|
+
const isItemField = !!arrayPath;
|
|
90
|
+
const indexMatch = isItemField ? dataPath.match(/\[(\d+)\]/) : undefined;
|
|
91
|
+
const index = indexMatch ? Number(indexMatch[1]) : undefined;
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
get formData() {
|
|
95
|
+
return form.getValues();
|
|
96
|
+
},
|
|
97
|
+
get rootValue() {
|
|
98
|
+
return form.getValues();
|
|
99
|
+
},
|
|
100
|
+
value: state?.value,
|
|
101
|
+
dataPath,
|
|
102
|
+
path: dataPath,
|
|
103
|
+
index,
|
|
104
|
+
parentValues: arrayPath ? form.getValueByPath(arrayPath) : undefined,
|
|
105
|
+
getValue: (p: string) => form.getValueByPath(p),
|
|
106
|
+
setValue: (p: string, v: unknown) => form.setValueByPath(p, v),
|
|
107
|
+
onItemChange: (p: string, v: unknown) => form.setValueByPath(p, v),
|
|
108
|
+
validate: async (p?: string) => {
|
|
109
|
+
if (p) {
|
|
110
|
+
await form.validateFields([p]);
|
|
111
|
+
} else {
|
|
112
|
+
await form.validateFields([dataPath]);
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
validateFields: async (paths?: string[]) => {
|
|
116
|
+
await form.validateFields(paths);
|
|
117
|
+
},
|
|
118
|
+
submit: () => form.submit(),
|
|
119
|
+
resetFields: () => form.resetFields(),
|
|
120
|
+
setSchema: (s: Record<string, unknown>) =>
|
|
121
|
+
form.setSchema(s as any),
|
|
122
|
+
setSchemaByPath: (p: string, patch: Record<string, unknown>) =>
|
|
123
|
+
form.setSchemaByPath(p, patch),
|
|
124
|
+
getSchema: () => form.getSchema(),
|
|
125
|
+
};
|
|
126
|
+
}, [form, dataPath, state?.value, state?.meta.itemOf]);
|
|
127
|
+
|
|
85
128
|
if (!state) {
|
|
86
129
|
// 仅当引擎已初始化(version > 0)但字段仍未找到时才发出警告
|
|
87
130
|
// 初始化过程中的短暂空状态不应报警
|
|
@@ -146,37 +189,71 @@ export function NexusField({ dataPath, layoutKey }: NexusFieldProps) {
|
|
|
146
189
|
...(effectiveColSpan ? { gridColumn: `span ${effectiveColSpan}` } : {}),
|
|
147
190
|
};
|
|
148
191
|
|
|
192
|
+
// 默认包裹:所有 widget 统一由 FieldWrapper 包裹(引擎注册,UI 层提供),
|
|
193
|
+
// 仅当 label === false(字段级或表单级)时 FieldWrapper 不包裹 Form.Item。
|
|
194
|
+
// 未注册 FieldWrapper(纯 react 无 ui 层)时直接渲染裸 widget。
|
|
195
|
+
const FieldWrapper = engine.getFieldWrapper();
|
|
196
|
+
|
|
197
|
+
// Form.Item 消费的元数据 props 剥离给 FieldWrapper,避免透传到底层 antd 控件:
|
|
198
|
+
// - required: 会让 <input required> 触发浏览器原生校验
|
|
199
|
+
// - errors/title/description/label/extra/width/displayType/labelWidth/column:
|
|
200
|
+
// 作为未知属性透传到 DOM 会产生 React 警告
|
|
201
|
+
// width / colSpan / displayType / labelWidth / column 是布局属性,
|
|
202
|
+
// 由 NexusField 外层 <div> 的 wrapperStyle 统一消费,不透传给 FieldWrapper(Form.Item),
|
|
203
|
+
// 避免外层 div 与 Form.Item 重复设置 width。
|
|
204
|
+
const fieldWrapperProps = {
|
|
205
|
+
label: state.meta.label,
|
|
206
|
+
title: state.meta.title,
|
|
207
|
+
description: state.meta.description,
|
|
208
|
+
tooltip: state.meta.tooltip,
|
|
209
|
+
errors: state.errors,
|
|
210
|
+
required: state.required,
|
|
211
|
+
extra: state.meta.extra,
|
|
212
|
+
displayType: fieldDisplayType,
|
|
213
|
+
labelWidth: fieldLabelWidth,
|
|
214
|
+
column: fieldColumn,
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
// widget 仅接收控件相关 props(value/onChange/状态/选项/表单引用/自有 props)
|
|
218
|
+
const widgetProps = {
|
|
219
|
+
dataPath,
|
|
220
|
+
path: dataPath,
|
|
221
|
+
value: state.value,
|
|
222
|
+
onChange: handleChange,
|
|
223
|
+
disabled,
|
|
224
|
+
readOnly,
|
|
225
|
+
loading: state.loading,
|
|
226
|
+
placeholder: state.meta.placeholder,
|
|
227
|
+
options,
|
|
228
|
+
form,
|
|
229
|
+
addons,
|
|
230
|
+
dependValues,
|
|
231
|
+
items: state.meta.items,
|
|
232
|
+
schema: state.meta.schema,
|
|
233
|
+
// 远程选项数据版本:reloadRemoteData 后变化,widget 据此跳过缓存重新请求
|
|
234
|
+
remoteVersion: engine.getRemoteDataVersion(dataPath),
|
|
235
|
+
...state.props,
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
let control: ReactElement;
|
|
239
|
+
if (FieldWrapper) {
|
|
240
|
+
control = (
|
|
241
|
+
<FieldWrapper key={layoutKey} {...fieldWrapperProps}>
|
|
242
|
+
<Widget {...widgetProps} />
|
|
243
|
+
</FieldWrapper>
|
|
244
|
+
);
|
|
245
|
+
} else {
|
|
246
|
+
control = <Widget key={layoutKey} {...widgetProps} />;
|
|
247
|
+
}
|
|
248
|
+
|
|
149
249
|
return (
|
|
150
250
|
<div
|
|
151
251
|
data-nexus-field={dataPath}
|
|
252
|
+
className={readOnly ? 'nexus-field-readonly' : undefined}
|
|
152
253
|
onBlur={handleBlur}
|
|
153
254
|
style={Object.keys(wrapperStyle).length > 0 ? wrapperStyle : undefined}
|
|
154
255
|
>
|
|
155
|
-
|
|
156
|
-
key={layoutKey}
|
|
157
|
-
dataPath={dataPath}
|
|
158
|
-
path={dataPath}
|
|
159
|
-
value={state.value}
|
|
160
|
-
onChange={handleChange}
|
|
161
|
-
disabled={disabled}
|
|
162
|
-
readOnly={readOnly}
|
|
163
|
-
loading={state.loading}
|
|
164
|
-
required={state.required}
|
|
165
|
-
title={state.meta.title}
|
|
166
|
-
description={state.meta.description}
|
|
167
|
-
placeholder={state.meta.placeholder}
|
|
168
|
-
label={state.meta.label}
|
|
169
|
-
options={options}
|
|
170
|
-
errors={state.errors}
|
|
171
|
-
extra={state.meta.extra}
|
|
172
|
-
displayType={fieldDisplayType}
|
|
173
|
-
labelWidth={fieldLabelWidth}
|
|
174
|
-
column={fieldColumn}
|
|
175
|
-
form={form}
|
|
176
|
-
dependValues={dependValues}
|
|
177
|
-
items={state.meta.items}
|
|
178
|
-
{...state.props}
|
|
179
|
-
/>
|
|
256
|
+
{control}
|
|
180
257
|
</div>
|
|
181
258
|
);
|
|
182
259
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { NexusEngine } from '@xbeeant/form-engine';
|
|
2
2
|
import type { ReactNode } from 'react';
|
|
3
3
|
import { useMemo } from 'react';
|
|
4
|
-
import { NexusContext } from '../contexts/
|
|
5
|
-
import type { FormController } from './
|
|
6
|
-
import type { NexusFormConfig } from './
|
|
4
|
+
import { NexusContext } from '../contexts/nexus-context';
|
|
5
|
+
import type { FormController } from './form-controller';
|
|
6
|
+
import type { NexusFormConfig } from './nexus-form';
|
|
7
7
|
|
|
8
8
|
interface NexusFormProviderProps {
|
|
9
9
|
engine: NexusEngine;
|
|
@@ -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,10 +57,19 @@ 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
|
/** 校验失败回调 */
|
|
50
65
|
onFinishFailed?: (errors: Map<string, string[]>) => void;
|
|
66
|
+
/**
|
|
67
|
+
* 表单首次加载回调:非空 schema 首次传入并完成渲染后执行一次
|
|
68
|
+
* - undefined / null / {}(无 properties)均视为「空」schema,不触发
|
|
69
|
+
* - schema 由空变为非空时,于首个非空渲染提交后触发
|
|
70
|
+
* - 后续 schema 变更不重复触发
|
|
71
|
+
*/
|
|
72
|
+
onMount?: () => void;
|
|
51
73
|
/** 是否显示默认 footer(提交/重置按钮),或自定义 footer */
|
|
52
74
|
footer?: boolean | ReactNode;
|
|
53
75
|
/** 自定义类名 */
|
|
@@ -70,12 +92,37 @@ export interface NexusFormProps {
|
|
|
70
92
|
changedPath?: string,
|
|
71
93
|
) => void;
|
|
72
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;
|
|
73
105
|
/**
|
|
74
106
|
* 提交时是否移除 hidden 字段数据,默认 true
|
|
75
107
|
* - true: submit/getValues 不包含 hidden 字段
|
|
76
108
|
* - false: submit/getValues 包含所有字段(含 hidden)
|
|
77
109
|
*/
|
|
78
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;
|
|
79
126
|
|
|
80
127
|
// ── 表单布局配置 ──────────────────────────────────────────────────────
|
|
81
128
|
/** label 列配置(由 ui 层 Form.Item 消费) */
|
|
@@ -105,8 +152,10 @@ export function NexusForm({
|
|
|
105
152
|
initialValues,
|
|
106
153
|
widgets,
|
|
107
154
|
layouts,
|
|
155
|
+
fieldWrapper,
|
|
108
156
|
onFinish,
|
|
109
157
|
onFinishFailed,
|
|
158
|
+
onMount,
|
|
110
159
|
footer = false,
|
|
111
160
|
className,
|
|
112
161
|
style,
|
|
@@ -119,10 +168,24 @@ export function NexusForm({
|
|
|
119
168
|
readOnly,
|
|
120
169
|
column,
|
|
121
170
|
watch,
|
|
171
|
+
onValuesChange,
|
|
122
172
|
removeHiddenData = true,
|
|
173
|
+
omitNil = false,
|
|
174
|
+
locale,
|
|
175
|
+
persist,
|
|
123
176
|
}: NexusFormProps) {
|
|
124
|
-
|
|
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
|
+
);
|
|
125
185
|
const formElRef = useRef<HTMLFormElement | null>(null);
|
|
186
|
+
// persist 配置经 ref 持有:保存回调不随配置对象变化重建订阅
|
|
187
|
+
const persistRef = useRef<PersistOptions | undefined>(persist);
|
|
188
|
+
persistRef.current = persist;
|
|
126
189
|
|
|
127
190
|
// Schema 顶层配置作为默认值,props 优先级更高
|
|
128
191
|
const finalDisplayType = displayType ?? schema?.displayType ?? 'row';
|
|
@@ -131,16 +194,20 @@ export function NexusForm({
|
|
|
131
194
|
const finalLabelWidth = labelWidth ?? schema?.labelWidth;
|
|
132
195
|
const finalReadOnly = readOnly ?? schema?.readOnly ?? false;
|
|
133
196
|
const finalColumn = column ?? schema?.column;
|
|
197
|
+
const finalLocale = locale ?? schema?.locale ?? engine.getLocale();
|
|
134
198
|
|
|
135
199
|
// 注册额外 widgets / layouts(仅首次或引用变化时)
|
|
136
200
|
useEffect(() => {
|
|
137
201
|
if (widgets) {
|
|
138
202
|
engine.registerWidgets(widgets);
|
|
139
203
|
}
|
|
204
|
+
if (fieldWrapper) {
|
|
205
|
+
engine.registerFieldWrapper(fieldWrapper);
|
|
206
|
+
}
|
|
140
207
|
if (layouts) {
|
|
141
208
|
engine.registerLayouts(layouts);
|
|
142
209
|
}
|
|
143
|
-
}, [engine, widgets, layouts]);
|
|
210
|
+
}, [engine, widgets, layouts, fieldWrapper]);
|
|
144
211
|
|
|
145
212
|
// schema 变化时重新初始化
|
|
146
213
|
// 注意:initialValues 仅在首次挂载时使用,避免每次渲染都 re-init 导致
|
|
@@ -152,7 +219,11 @@ export function NexusForm({
|
|
|
152
219
|
return;
|
|
153
220
|
}
|
|
154
221
|
if (isFirstInitRef.current) {
|
|
155
|
-
|
|
222
|
+
// 草稿持久化:恢复已保存草稿(优先于 initialValues)
|
|
223
|
+
const persisted = persistRef.current
|
|
224
|
+
? loadPersisted(persistRef.current)
|
|
225
|
+
: undefined;
|
|
226
|
+
engine.init(schema, persisted ?? initialValuesRef.current);
|
|
156
227
|
isFirstInitRef.current = false;
|
|
157
228
|
} else {
|
|
158
229
|
// schema 变化:保留当前已填数据,而非重置为 initialValues
|
|
@@ -161,34 +232,99 @@ export function NexusForm({
|
|
|
161
232
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
162
233
|
}, [engine, schema]);
|
|
163
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
|
+
|
|
264
|
+
// onMount:首次传入「非空」schema 并完成渲染后执行一次
|
|
265
|
+
// undefined / null / {}(无任何键)均视为空 schema,不触发;
|
|
266
|
+
// schema 由空变为非空时,于首个非空渲染提交(useEffect)后触发。
|
|
267
|
+
const onMountRef = useRef(onMount);
|
|
268
|
+
onMountRef.current = onMount;
|
|
269
|
+
const onMountFiredRef = useRef(false);
|
|
270
|
+
const isSchemaEmpty =
|
|
271
|
+
!schema || (typeof schema === 'object' && Object.keys(schema).length === 0);
|
|
272
|
+
useEffect(() => {
|
|
273
|
+
if (onMountFiredRef.current || isSchemaEmpty) {
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
onMountFiredRef.current = true;
|
|
277
|
+
onMountRef.current?.();
|
|
278
|
+
}, [isSchemaEmpty]);
|
|
279
|
+
|
|
164
280
|
// 绑定 form controller
|
|
165
281
|
// 使用 ref 持有 onFinish / onFinishFailed,避免每次 re-render 都造成绑定逻辑重复执行
|
|
166
282
|
const onFinishRef =
|
|
167
283
|
useRef<(data: Record<string, unknown>) => void | Promise<void>>(noop);
|
|
168
284
|
const onFinishFailedRef =
|
|
169
285
|
useRef<(errors: Map<string, string[]>) => void>(noop);
|
|
170
|
-
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
|
+
};
|
|
171
294
|
onFinishFailedRef.current = onFinishFailed ?? noop;
|
|
172
295
|
|
|
173
296
|
// 只在挂载时绑定一次:传入「稳定的 getter」,让 FormController 在 submit 时读取最新回调
|
|
174
297
|
useEffect(() => {
|
|
175
298
|
form._bind(
|
|
299
|
+
instanceKey,
|
|
176
300
|
formElRef.current,
|
|
177
301
|
() => onFinishRef.current,
|
|
178
302
|
() => onFinishFailedRef.current,
|
|
179
303
|
);
|
|
180
304
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
181
|
-
}, [form]);
|
|
305
|
+
}, [form, instanceKey]);
|
|
182
306
|
|
|
183
|
-
// watch / removeHiddenData 变化时单独同步(不重置其他绑定)
|
|
307
|
+
// watch / removeHiddenData / onValuesChange 变化时单独同步(不重置其他绑定)
|
|
308
|
+
// onValuesChange 经 ref 持有:回调内容变化不重建订阅,仅内部引用更新
|
|
309
|
+
const onValuesChangeRef = useRef(onValuesChange);
|
|
310
|
+
onValuesChangeRef.current = onValuesChange;
|
|
184
311
|
useEffect(() => {
|
|
185
|
-
form._syncConfig(
|
|
186
|
-
|
|
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]);
|
|
187
320
|
|
|
321
|
+
// 渲染树版本订阅:仅 Schema 结构变化(init/setSchema/reset)时重算 renderTree。
|
|
322
|
+
// 字段值/错误等数据变化只 bump store 版本(useFormData 消费),
|
|
323
|
+
// 不会触发 NexusForm 重渲染——各字段经字段级版本订阅精准重渲染。
|
|
188
324
|
const _version = useSyncExternalStore(
|
|
189
|
-
engine.
|
|
190
|
-
engine.
|
|
191
|
-
engine.
|
|
325
|
+
(cb) => engine.subscribeRender(cb),
|
|
326
|
+
() => engine.getRenderSnapshot(),
|
|
327
|
+
() => engine.getRenderSnapshot(),
|
|
192
328
|
);
|
|
193
329
|
// 依赖 _version:engine.init() / setSchema() 会 bump version,
|
|
194
330
|
// 需要在此后重新读取 renderTree(首次渲染时 engine 尚未 init,renderTree 为空)
|
|
@@ -208,11 +344,14 @@ export function NexusForm({
|
|
|
208
344
|
}, [form]);
|
|
209
345
|
|
|
210
346
|
// footer 渲染
|
|
347
|
+
const submitting = useFormSubmitting(form);
|
|
211
348
|
let footerNode: ReactNode = null;
|
|
212
349
|
if (footer === true) {
|
|
213
350
|
footerNode = (
|
|
214
351
|
<div className='mt-4'>
|
|
215
|
-
<button type='submit'
|
|
352
|
+
<button type='submit' disabled={submitting}>
|
|
353
|
+
{submitting ? '提交中...' : '提交'}
|
|
354
|
+
</button>{' '}
|
|
216
355
|
<button type='button' onClick={handleReset}>
|
|
217
356
|
重置
|
|
218
357
|
</button>
|
|
@@ -250,6 +389,7 @@ export function NexusForm({
|
|
|
250
389
|
displayType: finalDisplayType,
|
|
251
390
|
readOnly: finalReadOnly,
|
|
252
391
|
column: finalColumn,
|
|
392
|
+
locale: finalLocale,
|
|
253
393
|
}),
|
|
254
394
|
[
|
|
255
395
|
mergedLabelCol,
|
|
@@ -259,6 +399,7 @@ export function NexusForm({
|
|
|
259
399
|
finalDisplayType,
|
|
260
400
|
finalReadOnly,
|
|
261
401
|
finalColumn,
|
|
402
|
+
finalLocale,
|
|
262
403
|
],
|
|
263
404
|
);
|
|
264
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
|
+
}
|