@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.
Files changed (142) hide show
  1. package/dist/cjs/components/FormController.cjs +1 -1
  2. package/dist/cjs/components/NexusField.cjs +1 -1
  3. package/dist/cjs/components/NexusForm.cjs +1 -1
  4. package/dist/cjs/components/form-controller.cjs +1 -0
  5. package/dist/cjs/components/nexus-field.cjs +1 -0
  6. package/dist/cjs/components/nexus-form-provider.cjs +1 -0
  7. package/dist/cjs/components/nexus-form.cjs +1 -0
  8. package/dist/cjs/components/nexus-layout.cjs +1 -0
  9. package/dist/cjs/components/nexus-object.cjs +1 -0
  10. package/dist/cjs/contexts/field-inherit-context.cjs +1 -0
  11. package/dist/cjs/contexts/grid-context.cjs +1 -0
  12. package/dist/cjs/contexts/layout-config-context.cjs +1 -0
  13. package/dist/cjs/contexts/nexus-context.cjs +1 -0
  14. package/dist/cjs/hooks/index.cjs +1 -1
  15. package/dist/cjs/hooks/use-engine.cjs +1 -0
  16. package/dist/cjs/hooks/use-field-state.cjs +1 -0
  17. package/dist/cjs/hooks/use-field-validator.cjs +1 -0
  18. package/dist/cjs/hooks/use-field-value.cjs +1 -0
  19. package/dist/cjs/hooks/use-form-config.cjs +1 -0
  20. package/dist/cjs/hooks/use-form-data.cjs +1 -0
  21. package/dist/cjs/hooks/use-form-submitting.cjs +1 -0
  22. package/dist/cjs/hooks/use-form.cjs +1 -0
  23. package/dist/cjs/hooks/use-nexus-context.cjs +1 -0
  24. package/dist/cjs/hooks/use-watch-all.cjs +1 -0
  25. package/dist/cjs/hooks/use-watch-multiple.cjs +1 -0
  26. package/dist/cjs/hooks/use-watch-state.cjs +1 -0
  27. package/dist/cjs/hooks/use-watch.cjs +1 -0
  28. package/dist/cjs/hooks/useFormData.cjs +1 -1
  29. package/dist/cjs/hooks/useFormSubmitting.cjs +1 -0
  30. package/dist/cjs/index.cjs +1 -1
  31. package/dist/cjs/styles.css +1 -1
  32. package/dist/cjs/utils/omit-nil.cjs +1 -0
  33. package/dist/cjs/utils/omitNil.cjs +1 -0
  34. package/dist/cjs/utils/persist.cjs +1 -0
  35. package/dist/cjs/utils/render-tree-node.cjs +1 -0
  36. package/dist/cjs/utils/resolve-col-span.cjs +1 -0
  37. package/dist/es/components/FormController.d.ts +73 -19
  38. package/dist/es/components/FormController.js +222 -71
  39. package/dist/es/components/NexusField.js +25 -21
  40. package/dist/es/components/NexusForm.d.ts +33 -3
  41. package/dist/es/components/NexusForm.js +143 -96
  42. package/dist/es/components/form-controller.d.ts +129 -0
  43. package/dist/es/components/form-controller.js +310 -0
  44. package/dist/es/components/nexus-field.d.ts +9 -0
  45. package/dist/es/components/nexus-field.js +131 -0
  46. package/dist/es/components/nexus-form-provider.d.ts +12 -0
  47. package/dist/es/components/nexus-form-provider.js +18 -0
  48. package/dist/es/components/nexus-form.d.ts +114 -0
  49. package/dist/es/components/nexus-form.js +174 -0
  50. package/dist/es/components/nexus-layout.d.ts +9 -0
  51. package/dist/es/components/nexus-layout.js +48 -0
  52. package/dist/es/components/nexus-object.d.ts +16 -0
  53. package/dist/es/components/nexus-object.js +64 -0
  54. package/dist/es/contexts/NexusContext.d.ts +2 -0
  55. package/dist/es/contexts/field-inherit-context.d.ts +18 -0
  56. package/dist/es/contexts/field-inherit-context.js +5 -0
  57. package/dist/es/contexts/grid-context.d.ts +5 -0
  58. package/dist/es/contexts/grid-context.js +5 -0
  59. package/dist/es/contexts/layout-config-context.d.ts +4 -0
  60. package/dist/es/contexts/layout-config-context.js +5 -0
  61. package/dist/es/contexts/nexus-context.d.ts +29 -0
  62. package/dist/es/contexts/nexus-context.js +12 -0
  63. package/dist/es/hooks/index.d.ts +12 -11
  64. package/dist/es/hooks/index.js +20 -18
  65. package/dist/es/hooks/use-engine.d.ts +2 -0
  66. package/dist/es/hooks/use-engine.js +8 -0
  67. package/dist/es/hooks/use-field-state.d.ts +7 -0
  68. package/dist/es/hooks/use-field-state.js +13 -0
  69. package/dist/es/hooks/use-field-validator.d.ts +40 -0
  70. package/dist/es/hooks/use-field-validator.js +35 -0
  71. package/dist/es/hooks/use-field-value.d.ts +6 -0
  72. package/dist/es/hooks/use-field-value.js +13 -0
  73. package/dist/es/hooks/use-form-config.d.ts +5 -0
  74. package/dist/es/hooks/use-form-config.js +8 -0
  75. package/dist/es/hooks/use-form-data.d.ts +6 -0
  76. package/dist/es/hooks/use-form-data.js +13 -0
  77. package/dist/es/hooks/use-form-submitting.d.ts +7 -0
  78. package/dist/es/hooks/use-form-submitting.js +11 -0
  79. package/dist/es/hooks/use-form.d.ts +22 -0
  80. package/dist/es/hooks/use-form.js +12 -0
  81. package/dist/es/hooks/use-nexus-context.d.ts +11 -0
  82. package/dist/es/hooks/use-nexus-context.js +12 -0
  83. package/dist/es/hooks/use-watch-all.d.ts +19 -0
  84. package/dist/es/hooks/use-watch-all.js +29 -0
  85. package/dist/es/hooks/use-watch-multiple.d.ts +20 -0
  86. package/dist/es/hooks/use-watch-multiple.js +29 -0
  87. package/dist/es/hooks/use-watch-state.d.ts +18 -0
  88. package/dist/es/hooks/use-watch-state.js +19 -0
  89. package/dist/es/hooks/use-watch.d.ts +25 -0
  90. package/dist/es/hooks/use-watch.js +20 -0
  91. package/dist/es/hooks/useForm.d.ts +11 -4
  92. package/dist/es/hooks/useFormData.js +9 -9
  93. package/dist/es/hooks/useFormSubmitting.d.ts +7 -0
  94. package/dist/es/hooks/useFormSubmitting.js +11 -0
  95. package/dist/es/index.d.ts +26 -24
  96. package/dist/es/index.js +36 -32
  97. package/dist/es/styles.css +1 -1
  98. package/dist/es/utils/omit-nil.d.ts +9 -0
  99. package/dist/es/utils/omit-nil.js +14 -0
  100. package/dist/es/utils/omitNil.d.ts +9 -0
  101. package/dist/es/utils/omitNil.js +14 -0
  102. package/dist/es/utils/persist.d.ts +24 -0
  103. package/dist/es/utils/persist.js +43 -0
  104. package/dist/es/utils/render-tree-node.d.ts +6 -0
  105. package/dist/es/utils/render-tree-node.js +17 -0
  106. package/dist/es/utils/resolve-col-span.d.ts +5 -0
  107. package/dist/es/utils/resolve-col-span.js +9 -0
  108. package/dist/umd/index.css +1 -1
  109. package/dist/umd/index.umd.cjs +1 -1
  110. package/package.json +5 -4
  111. package/src/components/form-controller.ts +584 -0
  112. package/src/components/{NexusField.tsx → nexus-field.tsx} +57 -6
  113. package/src/components/{NexusFormProvider.tsx → nexus-form-provider.tsx} +3 -3
  114. package/src/components/{NexusForm.tsx → nexus-form.tsx} +135 -18
  115. package/src/components/{NexusLayout.tsx → nexus-layout.tsx} +5 -5
  116. package/src/components/{NexusObject.tsx → nexus-object.tsx} +3 -3
  117. package/src/contexts/{NexusContext.ts → nexus-context.ts} +3 -1
  118. package/src/hooks/index.ts +12 -11
  119. package/src/hooks/{useEngine.ts → use-engine.ts} +1 -1
  120. package/src/hooks/{useFieldState.ts → use-field-state.ts} +1 -1
  121. package/src/hooks/{useFieldValidator.ts → use-field-validator.ts} +1 -1
  122. package/src/hooks/{useFieldValue.ts → use-field-value.ts} +1 -1
  123. package/src/hooks/{useFormConfig.ts → use-form-config.ts} +2 -2
  124. package/src/hooks/{useFormData.ts → use-form-data.ts} +4 -4
  125. package/src/hooks/use-form-submitting.ts +16 -0
  126. package/src/hooks/{useForm.ts → use-form.ts} +12 -5
  127. package/src/hooks/{useNexusContext.ts → use-nexus-context.ts} +2 -2
  128. package/src/index.ts +26 -24
  129. package/src/utils/omit-nil.ts +24 -0
  130. package/src/utils/persist.ts +88 -0
  131. package/src/utils/{renderTreeNode.tsx → render-tree-node.tsx} +3 -3
  132. package/src/utils/{resolveColSpan.ts → resolve-col-span.ts} +1 -1
  133. package/dist/cjs/tailwind.css +0 -1
  134. package/dist/es/tailwind.css +0 -1
  135. package/src/components/FormController.ts +0 -278
  136. /package/src/contexts/{FieldInheritContext.ts → field-inherit-context.ts} +0 -0
  137. /package/src/contexts/{GridContext.ts → grid-context.ts} +0 -0
  138. /package/src/contexts/{LayoutConfigContext.ts → layout-config-context.ts} +0 -0
  139. /package/src/hooks/{useWatchAll.ts → use-watch-all.ts} +0 -0
  140. /package/src/hooks/{useWatchMultiple.ts → use-watch-multiple.ts} +0 -0
  141. /package/src/hooks/{useWatchState.ts → use-watch-state.ts} +0 -0
  142. /package/src/hooks/{useWatch.ts → use-watch.ts} +0 -0
@@ -1,76 +1,173 @@
1
- import { NexusEngine as l, AsyncValidatorPlugin as o } from "@xbeeant/form-engine";
2
- class c {
1
+ import { NexusEngine as g, AsyncValidatorPlugin as h } from "@xbeeant/form-engine";
2
+ import { omitNilDeep as c } from "../utils/omitNil.js";
3
+ const f = /* @__PURE__ */ new WeakSet();
4
+ class V {
3
5
  engine;
4
- formElementRef;
5
- /** 稳定的 getter:每次 submit 执行时从外部 ref 读取最新的 onFinish/onFinishFailed 回调 */
6
- getOnFinish;
7
- getOnFinishFailed;
8
- removeHiddenData = !0;
9
- watchers = /* @__PURE__ */ new Map();
10
- globalWatcher = null;
6
+ /** 提交中状态(校验 + onFinish 全流程,formily submitting 对齐) */
7
+ submitting = !1;
8
+ /** 提交状态监听器(供 useSyncExternalStore 消费) */
9
+ submittingListeners = /* @__PURE__ */ new Set();
10
+ /** 实例标识(内部)→ 实例视图引擎(同一 form 挂载的多个 NexusForm 各自独立) */
11
+ views = /* @__PURE__ */ new Map();
12
+ /** 实例标识 → 该实例的 DOM / 回调绑定(onFinish/onFinishFailed 按实例独立) */
13
+ instanceBindings = /* @__PURE__ */ new Map();
14
+ /** 实例标识 → watch / removeHiddenData / onValuesChange / omitNil 配置(按实例独立) */
15
+ instanceConfigs = /* @__PURE__ */ new Map();
11
16
  constructor(e) {
12
- this.engine = e ?? new l(), this.engine.hasPlugin("async-validator") || this.engine.use(new o(this.engine)), this.formElementRef = {
13
- current: null
14
- }, this.getOnFinish = () => () => {
15
- }, this.getOnFinishFailed = () => () => {
16
- };
17
+ this.engine = e ?? new g(), this.engine.hasPlugin("async-validator") || this.engine.use(new h(this.engine));
18
+ }
19
+ /**
20
+ * 获取(或创建)指定实例的引擎视图
21
+ *
22
+ * 同一 form 挂载多个 NexusForm 时各自独立:schema/值/订阅互不影响。
23
+ * 实例标识由 NexusForm 内部分配(React useId),用户不感知。
24
+ *
25
+ * @param instanceId - 实例标识(内部)
26
+ * @returns 定向到该实例的引擎视图
27
+ */
28
+ _useInstance(e) {
29
+ let t = this.views.get(e);
30
+ return t || (this.views.size === 0 && !f.has(this.engine) ? (f.add(this.engine), t = this.engine) : t = this.engine.instance(e), this.views.set(e, t), this.instanceConfigs.set(e, {
31
+ removeHiddenData: !0,
32
+ omitNil: !1,
33
+ watchers: /* @__PURE__ */ new Map(),
34
+ globalWatcher: null,
35
+ onValuesChange: null
36
+ })), t;
37
+ }
38
+ /**
39
+ * 内部:解析方法作用的目标实例视图(聚合全部实例,无实例时回退宿主引擎)
40
+ */
41
+ resolveViews() {
42
+ const e = Array.from(this.views.values());
43
+ return e.length > 0 ? e : [this.engine];
44
+ }
45
+ /** 内部:首次绑定的实例 DOM(兼容既有 scrollToPath/focusFirstError 单实例行为) */
46
+ getPrimaryFormEl() {
47
+ return this.instanceBindings.values().next().value?.formEl ?? null;
17
48
  }
18
- /** 内部:首次绑定表单 DOM + 回调 getter(由 NexusForm 在挂载时调用一次) */
19
- _bind(e, t, i) {
20
- this.formElementRef.current = e, this.getOnFinish = t, this.getOnFinishFailed = i, this.engine.registerOnFieldValueChange(
21
- (n, r) => this._onFieldValueChange(n, r)
49
+ /**
50
+ * 内部:绑定实例 DOM + 回调 getter(由 NexusForm 在挂载时按实例调用)
51
+ * 回调以 getter 传入:submit 时读取最新闭包,而非绑定时刻的快照
52
+ */
53
+ _bind(e, t, s, n) {
54
+ this.instanceBindings.set(e, {
55
+ formEl: t,
56
+ getOnFinish: s,
57
+ getOnFinishFailed: n
58
+ }), this._useInstance(e).registerOnFieldValueChange(
59
+ (o, r) => this._onFieldValueChange(e, o, r)
22
60
  );
23
61
  }
24
- /** 内部:同步 watch / removeHiddenData 配置(由 NexusForm 在它们变化时调用) */
25
- _syncConfig(e) {
26
- if (e.removeHiddenData !== void 0 && (this.removeHiddenData = e.removeHiddenData), e.watch) {
27
- this.watchers.clear(), this.globalWatcher = null;
28
- for (const [t, i] of Object.entries(e.watch))
29
- t === "#" ? this.globalWatcher = i : this.watchers.set(t, i);
62
+ /** 内部:同步 watch / removeHiddenData / onValuesChange 配置(由 NexusForm 在它们变化时按实例调用) */
63
+ _syncConfig(e, t) {
64
+ const s = this.instanceConfigs.get(e) ?? {
65
+ removeHiddenData: !0,
66
+ omitNil: !1,
67
+ watchers: /* @__PURE__ */ new Map(),
68
+ globalWatcher: null,
69
+ onValuesChange: null
70
+ };
71
+ if (t.removeHiddenData !== void 0 && (s.removeHiddenData = t.removeHiddenData), t.omitNil !== void 0 && (s.omitNil = t.omitNil), t.onValuesChange !== void 0 && (s.onValuesChange = t.onValuesChange), t.watch) {
72
+ s.watchers.clear(), s.globalWatcher = null;
73
+ for (const [n, i] of Object.entries(t.watch))
74
+ n === "#" ? s.globalWatcher = i : s.watchers.set(n, i);
30
75
  }
76
+ this.instanceConfigs.set(e, s);
31
77
  }
32
- /** 内部:值变更时调用(由 Engine 通知) */
33
- _onFieldValueChange(e, t) {
34
- const i = this.engine.getFormData(), n = this.removeHiddenData ? i : this.engine.getAllFormData();
35
- this.globalWatcher && this.globalWatcher(n, n, e);
36
- const r = this.watchers.get(e);
37
- r && r(t, n);
78
+ /** 内部:值变更时调用(由 Engine 通知,携带实例标识路由到对应实例的 watcher) */
79
+ _onFieldValueChange(e, t, s) {
80
+ const n = this.views.get(e), i = this.instanceConfigs.get(e);
81
+ if (!n || !i)
82
+ return;
83
+ const o = n.getFormData(), r = i.removeHiddenData ? o : n.getAllFormData();
84
+ i.globalWatcher && i.globalWatcher(r, r, t), i.onValuesChange && i.onValuesChange(s, r, t);
85
+ const a = i.watchers.get(t);
86
+ a && a(s, r);
38
87
  }
39
- /** 内部:获取 Engine 实例 */
88
+ /** 内部:获取引擎宿主实例 */
40
89
  _getEngine() {
41
90
  return this.engine;
42
91
  }
43
92
  /**
44
- * 获取底层 Engine 实例(用于跨表单联动:linkForm / setFormId 等)
93
+ * 获取底层引擎宿主实例(用于跨表单联动:linkForm / setFormId / registerAntdUI 等;
94
+ * 宿主上的组件/插件注册对全部实例生效)
45
95
  */
46
96
  getEngine() {
47
97
  return this.engine;
48
98
  }
49
- async submit() {
50
- const e = await this.engine.validate();
51
- if (e.size > 0) {
52
- this.focusFirstError(e), this.getOnFinishFailed()?.(e);
99
+ async submit(e) {
100
+ this.setSubmitting(!0);
101
+ try {
102
+ await this.runSubmit(e);
103
+ } finally {
104
+ this.setSubmitting(!1);
105
+ }
106
+ }
107
+ /** 提交中状态(formily submitting 对齐,供提交按钮 loading) */
108
+ getSubmitting() {
109
+ return this.submitting;
110
+ }
111
+ /** 订阅提交状态变化(返回取消订阅函数,供 useSyncExternalStore 使用) */
112
+ onSubmittingChange(e) {
113
+ return this.submittingListeners.add(e), () => {
114
+ this.submittingListeners.delete(e);
115
+ };
116
+ }
117
+ setSubmitting(e) {
118
+ if (this.submitting !== e) {
119
+ this.submitting = e;
120
+ for (const t of this.submittingListeners)
121
+ t();
122
+ }
123
+ }
124
+ async runSubmit(e) {
125
+ const t = this.resolveViews(), s = /* @__PURE__ */ new Map();
126
+ let n;
127
+ for (const i of t) {
128
+ const o = await i.validate(void 0, {
129
+ validateFirst: e?.validateFirst
130
+ });
131
+ if (o.size > 0) {
132
+ for (const [r, a] of o)
133
+ s.set(r, a);
134
+ n ??= this.instanceBindings.get(this.findInstanceId(i));
135
+ }
136
+ }
137
+ if (s.size > 0) {
138
+ this.focusFirstError(s, n?.formEl ?? null), n?.getOnFinishFailed()?.(s);
53
139
  return;
54
140
  }
55
- const t = this.removeHiddenData ? this.engine.getFormData() : this.engine.getAllFormData();
56
- await this.getOnFinish()?.(t);
141
+ for (const i of t) {
142
+ const o = this.findInstanceId(i), r = this.instanceConfigs.get(o) ?? {
143
+ removeHiddenData: !0,
144
+ omitNil: !1
145
+ }, a = r.removeHiddenData ? i.getFormData() : i.getAllFormData(), l = e?.omitNil ?? r.omitNil ?? !1 ? c(a) : a;
146
+ await i.submit(l) && await this.instanceBindings.get(o)?.getOnFinish()?.(l);
147
+ }
148
+ }
149
+ /** 内部:反查实例视图对应的实例标识 */
150
+ findInstanceId(e) {
151
+ for (const [t, s] of this.views)
152
+ if (s === e)
153
+ return t;
154
+ return "default";
57
155
  }
58
156
  /**
59
157
  * 定位到第一个校验失败的字段:按 DOM 渲染顺序查找(保证视觉上的"第一个"),
60
158
  * 滚动入视并聚焦其内部可交互控件。
61
159
  */
62
- focusFirstError(e) {
63
- const t = this.formElementRef.current;
160
+ focusFirstError(e, t) {
64
161
  if (!t || e.size === 0)
65
162
  return;
66
- const i = new Set(e.keys());
163
+ const s = new Set(e.keys());
67
164
  requestAnimationFrame(() => {
68
- const r = t.querySelectorAll("[data-nexus-field]");
69
- for (const s of Array.from(r)) {
70
- const a = s.getAttribute("data-nexus-field");
71
- if (!a || !i.has(a))
165
+ const i = t.querySelectorAll("[data-nexus-field]");
166
+ for (const o of Array.from(i)) {
167
+ const r = o.getAttribute("data-nexus-field");
168
+ if (!r || !s.has(r))
72
169
  continue;
73
- s.scrollIntoView({ behavior: "smooth", block: "center" }), s.querySelector(
170
+ typeof o.scrollIntoView == "function" && o.scrollIntoView({ behavior: "smooth", block: "center" }), o.querySelector(
74
171
  'input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
75
172
  )?.focus();
76
173
  return;
@@ -78,35 +175,56 @@ class c {
78
175
  });
79
176
  }
80
177
  resetFields() {
81
- this.engine.reset();
178
+ for (const e of this.resolveViews())
179
+ e.reset();
82
180
  }
83
181
  setErrorFields(e) {
84
- this.engine.setErrorFields(e);
182
+ for (const t of this.resolveViews())
183
+ t.setErrorFields(e);
85
184
  }
86
185
  setValues(e) {
87
- this.engine.setFieldValues(e);
186
+ for (const t of this.resolveViews())
187
+ t.setFieldValues(e);
88
188
  }
89
189
  setValueByPath(e, t) {
90
- this.engine.setFieldValue(e, t);
190
+ for (const s of this.resolveViews())
191
+ s.setFieldValue(e, t);
91
192
  }
92
193
  setSchemaByPath(e, t) {
93
- this.engine.setSchemaByPath(e, t);
194
+ for (const s of this.resolveViews())
195
+ s.setSchemaByPath(e, t);
94
196
  }
95
197
  setSchema(e) {
96
- this.engine.setSchema(e);
198
+ for (const t of this.resolveViews())
199
+ t.setSchema(e);
97
200
  }
98
- getValues(e) {
99
- return this.engine.getFormData(e);
201
+ getValues(e, t) {
202
+ const s = {};
203
+ for (const n of this.resolveViews()) {
204
+ const i = n.getFormData(e);
205
+ t?.omitNil ? Object.assign(s, c(i)) : Object.assign(s, i);
206
+ }
207
+ return s;
100
208
  }
101
209
  getHiddenValues() {
102
- return this.engine.getHiddenValues();
210
+ const e = {};
211
+ for (const t of this.resolveViews())
212
+ Object.assign(e, t.getHiddenValues());
213
+ return e;
103
214
  }
104
215
  /** 获取所有字段值(含 hidden) */
105
216
  getAllValues() {
106
- return this.engine.getAllFormData();
217
+ const e = {};
218
+ for (const t of this.resolveViews())
219
+ Object.assign(e, t.getAllFormData());
220
+ return e;
107
221
  }
108
222
  getValueByPath(e) {
109
- return this.engine.getFieldValue(e);
223
+ for (const t of this.resolveViews()) {
224
+ const s = t.getFieldValue(e);
225
+ if (s !== void 0)
226
+ return s;
227
+ }
110
228
  }
111
229
  /**
112
230
  * 注册字段校验逻辑
@@ -114,46 +232,79 @@ class c {
114
232
  * @param validator 校验函数,返回错误消息数组(空数组表示通过)
115
233
  */
116
234
  registerValidator(e, t) {
117
- this.engine.registerFieldValidator(e, t);
235
+ for (const s of this.resolveViews())
236
+ s.registerFieldValidator(e, t);
118
237
  }
119
238
  /**
120
239
  * 注销字段校验逻辑(按函数引用移除)
121
240
  * 与 registerValidator 配对;widget 组件卸载时清理,避免校验器累积
122
241
  */
123
242
  unregisterValidator(e, t) {
124
- this.engine.unregisterFieldValidator(e, t);
243
+ for (const s of this.resolveViews())
244
+ s.unregisterFieldValidator(e, t);
125
245
  }
126
246
  /**
127
247
  * 实时重校验指定字段(同步)
128
248
  * 供 widget 组件内部状态变化(非字段值变化)时主动刷新错误态
129
249
  */
130
250
  revalidateField(e) {
131
- this.engine.validateField(e, { trigger: "change" });
251
+ for (const t of this.resolveViews())
252
+ t.validateField(e, { trigger: "change" });
132
253
  }
133
254
  getSchema() {
134
- return this.engine.getSchema();
255
+ return this.resolveViews()[0]?.getSchema() ?? null;
135
256
  }
136
257
  removeErrorField(e) {
137
- this.engine.removeErrorField(e);
258
+ for (const t of this.resolveViews())
259
+ t.removeErrorField(e);
138
260
  }
139
261
  scrollToPath(e) {
140
- this.formElementRef.current?.querySelector(
141
- `[data-nexus-field="${e}"]`
142
- )?.scrollIntoView({ behavior: "smooth", block: "center" });
262
+ this.getPrimaryFormEl()?.querySelector(`[data-nexus-field="${e}"]`)?.scrollIntoView({ behavior: "smooth", block: "center" });
143
263
  }
144
264
  getFieldError(e) {
145
- return this.engine.getFieldError(e);
265
+ for (const t of this.resolveViews()) {
266
+ const s = t.getFieldError(e);
267
+ if (s.length > 0)
268
+ return s;
269
+ }
270
+ return [];
146
271
  }
147
272
  getFieldsError() {
148
- return this.engine.getFieldsError();
273
+ const e = /* @__PURE__ */ new Map();
274
+ for (const t of this.resolveViews())
275
+ for (const [s, n] of t.getFieldsError())
276
+ e.set(s, n);
277
+ return e;
149
278
  }
150
- validateFields(e) {
151
- return this.engine.validate(e);
279
+ validateFields(e, t) {
280
+ const s = this.resolveViews();
281
+ return s.length === 1 ? s[0].validate(e, t) : Promise.all(s.map((n) => n.validate(e, t))).then(
282
+ (n) => {
283
+ const i = /* @__PURE__ */ new Map();
284
+ for (const o of n)
285
+ for (const [r, a] of o)
286
+ i.set(r, a);
287
+ return i;
288
+ }
289
+ );
152
290
  }
153
291
  getFieldState(e) {
154
- return this.engine.getFieldState(e);
292
+ for (const t of this.resolveViews()) {
293
+ const s = t.getFieldState(e);
294
+ if (s)
295
+ return s;
296
+ }
297
+ }
298
+ /**
299
+ * 重载远程选项数据(x-render reloadRemoteData 对齐)
300
+ * 传入 path 仅重载该字段的远程数据;缺省时重载全部远程数据字段。
301
+ * 作用于全部实例视图。
302
+ */
303
+ reloadRemoteData(e) {
304
+ for (const t of this.resolveViews())
305
+ t.reloadRemoteData(e);
155
306
  }
156
307
  }
157
308
  export {
158
- c as FormController
309
+ V as FormController
159
310
  };
@@ -1,18 +1,18 @@
1
- import { jsx as o, jsxs as L } from "react/jsx-runtime";
2
- import { useSyncExternalStore as $, useContext as m, useCallback as C, useMemo as W } from "react";
3
- import { FieldInheritContext as D } from "../contexts/FieldInheritContext.js";
4
- import { GridContext as E } from "../contexts/GridContext.js";
5
- import { LayoutConfigContext as G } from "../contexts/LayoutConfigContext.js";
6
- import { useNexusContext as H } from "../contexts/NexusContext.js";
7
- import { resolveColSpan as I } from "../utils/resolveColSpan.js";
1
+ import { jsx as r, jsxs as D } from "react/jsx-runtime";
2
+ import { useSyncExternalStore as L, useContext as m, useCallback as C, useMemo as h } from "react";
3
+ import { FieldInheritContext as R } from "../contexts/FieldInheritContext.js";
4
+ import { GridContext as $ } from "../contexts/GridContext.js";
5
+ import { LayoutConfigContext as E } from "../contexts/LayoutConfigContext.js";
6
+ import { useNexusContext as G } from "../contexts/NexusContext.js";
7
+ import { resolveColSpan as H } from "../utils/resolveColSpan.js";
8
8
  function X({ dataPath: t, layoutKey: c }) {
9
- const { engine: i, config: l, form: h } = H();
10
- $(
9
+ const { engine: i, config: l, form: W } = G();
10
+ L(
11
11
  (n) => i.subscribeField(t, n),
12
12
  () => i.getFieldVersion(t),
13
13
  () => i.getFieldVersion(t)
14
14
  );
15
- const e = i.getFieldState(t), w = m(E), F = m(G), d = m(D), v = C(
15
+ const e = i.getFieldState(t), w = m($), F = m(E), d = m(R), v = C(
16
16
  (n) => {
17
17
  i.setFieldValue(t, n);
18
18
  },
@@ -22,13 +22,13 @@ function X({ dataPath: t, layoutKey: c }) {
22
22
  n.currentTarget.contains(n.relatedTarget) || i.validateField(t, { trigger: "blur" });
23
23
  },
24
24
  [i, t]
25
- ), N = W(
25
+ ), N = h(
26
26
  () => e?.meta.enum ? e.meta.enum.map((n, s) => ({
27
27
  value: n,
28
28
  label: e.meta.enumNames?.[s] ?? String(n)
29
29
  })) : e?.props.options,
30
30
  [e?.meta.enum, e?.meta.enumNames, e?.props.options]
31
- ), O = W(() => {
31
+ ), O = h(() => {
32
32
  const n = {};
33
33
  if (e?.reactions) {
34
34
  for (const s of e.reactions)
@@ -41,24 +41,25 @@ function X({ dataPath: t, layoutKey: c }) {
41
41
  if (!e)
42
42
  return i.getSnapshot() > 0 && console.warn(`[NexusField] Field not found: ${t}`), null;
43
43
  if (d.visible === !1 || !e.visible)
44
- return F.removeHidden === !0 ? null : /* @__PURE__ */ o("div", { className: "hidden", "data-nexus-hidden": t });
45
- const p = l.readOnly || d.readOnly === !0 || e.readOnly, T = d.disabled === !0 || e.disabled, u = p && !!e.meta.readOnlyWidget, V = u ? e.meta.readOnlyWidget : e.meta.widget;
46
- let r = i.getWidget(V);
47
- if (!r && u && (r = i.getWidget(e.meta.widget)), !r)
48
- return /* @__PURE__ */ L("div", { className: "text-xs text-red-500", "data-nexus-field": t, children: [
44
+ return F.removeHidden === !0 ? null : /* @__PURE__ */ r("div", { className: "hidden", "data-nexus-hidden": t });
45
+ const p = l.readOnly || d.readOnly === !0 || e.readOnly, V = d.disabled === !0 || e.disabled, u = p && !!e.meta.readOnlyWidget, T = u ? e.meta.readOnlyWidget : e.meta.widget;
46
+ let o = i.getWidget(T);
47
+ if (!o && u && (o = i.getWidget(e.meta.widget)), !o)
48
+ return /* @__PURE__ */ D("div", { className: "text-xs text-red-500", "data-nexus-field": t, children: [
49
49
  '⚠️ Widget "',
50
50
  e.meta.widget,
51
51
  '" 未注册 (path: ',
52
52
  t,
53
53
  ")"
54
54
  ] });
55
- const j = e.meta.displayType ?? l.displayType, k = e.meta.labelWidth ?? l.labelWidth, q = e.meta.column ?? l.column, f = I(e.meta.colSpan, w), g = {
55
+ const j = e.meta.displayType ?? l.displayType, k = e.meta.labelWidth ?? l.labelWidth, q = e.meta.column ?? l.column, f = H(e.meta.colSpan, w), g = {
56
56
  ...e.meta.width ? { width: e.meta.width, flexShrink: 0 } : {},
57
57
  ...f ? { gridColumn: `span ${f}` } : {}
58
58
  }, x = i.getFieldWrapper(), B = {
59
59
  label: e.meta.label,
60
60
  title: e.meta.title,
61
61
  description: e.meta.description,
62
+ tooltip: e.meta.tooltip,
62
63
  errors: e.errors,
63
64
  required: e.required,
64
65
  extra: e.meta.extra,
@@ -71,18 +72,21 @@ function X({ dataPath: t, layoutKey: c }) {
71
72
  path: t,
72
73
  value: e.value,
73
74
  onChange: v,
74
- disabled: T,
75
+ disabled: V,
75
76
  readOnly: p,
76
77
  loading: e.loading,
77
78
  placeholder: e.meta.placeholder,
78
79
  options: N,
79
- form: h,
80
+ form: W,
80
81
  dependValues: O,
81
82
  items: e.meta.items,
83
+ schema: e.meta.schema,
84
+ // 远程选项数据版本:reloadRemoteData 后变化,widget 据此跳过缓存重新请求
85
+ remoteVersion: i.getRemoteDataVersion(t),
82
86
  ...e.props
83
87
  };
84
88
  let a;
85
- return x ? a = /* @__PURE__ */ o(x, { ...B, children: /* @__PURE__ */ o(r, { ...y }) }, c) : a = /* @__PURE__ */ o(r, { ...y }, c), /* @__PURE__ */ o(
89
+ return x ? a = /* @__PURE__ */ r(x, { ...B, children: /* @__PURE__ */ r(o, { ...y }) }, c) : a = /* @__PURE__ */ r(o, { ...y }, c), /* @__PURE__ */ r(
86
90
  "div",
87
91
  {
88
92
  "data-nexus-field": t,
@@ -1,5 +1,6 @@
1
- import { NexusSchema } from '@xbeeant/form-engine';
1
+ import { NexusComponent, NexusSchema } from '@xbeeant/form-engine';
2
2
  import { CSSProperties, ReactNode } from 'react';
3
+ import { PersistOptions } from '../utils/persist';
3
4
  import { FormController } from './FormController';
4
5
  export interface NexusFormConfig {
5
6
  /** label 列配置(由 ui 层 Form.Item 消费) */
@@ -16,9 +17,15 @@ export interface NexusFormConfig {
16
17
  readOnly?: boolean;
17
18
  /** 表单每行显示多少列 */
18
19
  column?: number;
20
+ /** 表单语言标识(如 'zh-CN' / 'en-US',ui 层消费:antd locale + 内置文案) */
21
+ locale?: string;
19
22
  }
20
23
  export interface NexusFormProps {
21
- /** Form 实例,由 useForm() 创建 */
24
+ /**
25
+ * Form 实例,由 useForm() 创建。同一 form 引用 = 同一引擎宿主(组件/插件注册共享);
26
+ * 每个 NexusForm 挂载自动获得独立实例(schema/值/订阅互不影响),
27
+ * form 的 API(getValues/setValues/submit...)聚合作用于全部实例。
28
+ */
22
29
  form: FormController;
23
30
  /** Schema 定义 */
24
31
  schema?: NexusSchema;
@@ -28,6 +35,8 @@ export interface NexusFormProps {
28
35
  widgets?: Record<string, (props: any) => ReactNode>;
29
36
  /** 额外注册的 layout */
30
37
  layouts?: Record<string, (props: any) => ReactNode>;
38
+ /** 字段包裹组件(UI 无关,Renderer 层注入;渲染层默认包裹所有 widget,label=false 时跳过) */
39
+ fieldWrapper?: NexusComponent;
31
40
  /** 提交成功回调 */
32
41
  onFinish?: (formData: Record<string, unknown>) => void | Promise<void>;
33
42
  /** 校验失败回调 */
@@ -57,12 +66,33 @@ export interface NexusFormProps {
57
66
  watch?: {
58
67
  [path: string]: (value: unknown, allValues: Record<string, unknown>, changedPath?: string) => void;
59
68
  };
69
+ /**
70
+ * 表单值变化回调(x-render onValuesChange 对齐)
71
+ * 任意字段值变化时触发,参数为 (changedValue, allValues, changedPath)
72
+ * 与 watch: { '#': fn } 语义等价,但作为标准回调 API 提供
73
+ */
74
+ onValuesChange?: (changedValue: unknown, allValues: Record<string, unknown>, changedPath: string) => void;
60
75
  /**
61
76
  * 提交时是否移除 hidden 字段数据,默认 true
62
77
  * - true: submit/getValues 不包含 hidden 字段
63
78
  * - false: submit/getValues 包含所有字段(含 hidden)
64
79
  */
65
80
  removeHiddenData?: boolean;
81
+ /**
82
+ * 提交时递归移除空值(undefined/null/'',ProForm omitNil 对齐)
83
+ * 与 form.submit({ omitNil: true }) / form.getValues(paths, { omitNil: true }) 等价
84
+ */
85
+ omitNil?: boolean;
86
+ /** 表单语言标识(如 'zh-CN' / 'en-US',ui 层消费:antd locale + 内置文案) */
87
+ locale?: string;
88
+ /**
89
+ * 表单草稿持久化:值变化时自动保存到 Web Storage,下次挂载自动恢复
90
+ * @example
91
+ * ```tsx
92
+ * <NexusForm persist={{ key: 'apply-form', storage: 'localStorage' }} />
93
+ * ```
94
+ */
95
+ persist?: PersistOptions;
66
96
  /** label 列配置(由 ui 层 Form.Item 消费) */
67
97
  labelCol?: Record<string, unknown>;
68
98
  /** label 宽度(px 或 %),快捷方式 — 映射到 labelCol.style.width */
@@ -81,4 +111,4 @@ export interface NexusFormProps {
81
111
  /**
82
112
  * NexusForm — 顶层表单组件
83
113
  */
84
- export declare function NexusForm({ form, schema, initialValues, widgets, layouts, onFinish, onFinishFailed, onMount, footer, className, style, children, labelCol, labelWidth, colon, label, displayType, readOnly, column, watch, removeHiddenData, }: NexusFormProps): import("react").JSX.Element;
114
+ export declare function NexusForm({ form, schema, initialValues, widgets, layouts, fieldWrapper, onFinish, onFinishFailed, onMount, footer, className, style, children, labelCol, labelWidth, colon, label, displayType, readOnly, column, watch, onValuesChange, removeHiddenData, omitNil, locale, persist, }: NexusFormProps): import("react").JSX.Element;