Skip to content

Commit 8b85d87

Browse files
committed
feat(FormStore): support nested structures in form data handling
Closes NEXT_BUILDER-5249
1 parent c0b0701 commit 8b85d87

File tree

2 files changed

+208
-31
lines changed

2 files changed

+208
-31
lines changed

shared/form/src/FormStore.spec.ts

Lines changed: 158 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,12 @@ describe("FormStore", () => {
313313

314314
store.resetFields();
315315

316-
expect(store.getAllValues()).toEqual({});
316+
expect(store.getAllValues()).toStrictEqual({
317+
a: undefined,
318+
b: undefined,
319+
c: undefined,
320+
"validator-item": undefined,
321+
});
317322
});
318323

319324
it("scrollToField should work", () => {
@@ -334,4 +339,156 @@ describe("FormStore", () => {
334339

335340
expect(mockScrollTo).toHaveBeenCalledWith("a.scroll.to", null);
336341
});
342+
343+
test("support nested object and array structures", () => {
344+
const store = new FormStore();
345+
346+
store.setField("user.name", {
347+
name: "user.name",
348+
label: "用户名",
349+
validate: {
350+
required: true,
351+
},
352+
});
353+
store.setField("user.age", {
354+
name: "user.age",
355+
label: "年龄",
356+
validate: {
357+
type: "number",
358+
},
359+
});
360+
store.setField("items[0].name", {
361+
name: "items[0].name",
362+
label: "第一个项目名称",
363+
validate: {
364+
required: true,
365+
},
366+
});
367+
store.setField("items[1].name", {
368+
name: "items[1].name",
369+
label: "第二个项目名称",
370+
validate: {},
371+
});
372+
373+
// setInitValue
374+
store.setInitValue({
375+
user: {
376+
name: "test",
377+
age: 18,
378+
},
379+
items: [{ name: "item1" }, { name: "item2" }],
380+
});
381+
382+
// getFieldsValue
383+
expect(store.getFieldsValue("user.name")).toBe("test");
384+
expect(store.getFieldsValue("user.age")).toBe(18);
385+
expect(store.getFieldsValue("items[0].name")).toBe("item1");
386+
expect(store.getFieldsValue("items[1].name")).toBe("item2");
387+
388+
// getAllValues
389+
const allValues = store.getAllValues();
390+
expect(allValues).toEqual({
391+
user: {
392+
name: "test",
393+
age: 18,
394+
},
395+
items: [{ name: "item1" }, { name: "item2" }],
396+
});
397+
398+
// setFieldsValue
399+
store.setFieldsValue({
400+
user: {
401+
name: "updated",
402+
age: 20,
403+
},
404+
});
405+
406+
expect(store.getFieldsValue("user.name")).toBe("updated");
407+
expect(store.getFieldsValue("user.age")).toBe(20);
408+
409+
// validateFields
410+
const mockValidateFields = jest.fn();
411+
store.validateFields(mockValidateFields);
412+
413+
expect(mockValidateFields).toHaveBeenCalledWith(false, {
414+
user: {
415+
name: "updated",
416+
age: 20,
417+
},
418+
items: [{ name: "item1" }, { name: "item2" }],
419+
});
420+
421+
// resetFields
422+
store.resetFields("user.name");
423+
expect(store.getFieldsValue("user.name")).toBeUndefined();
424+
expect(store.getFieldsValue("user.age")).toBe(20);
425+
426+
store.resetFields();
427+
expect(store.getAllValues()).toStrictEqual({
428+
items: [
429+
{
430+
name: undefined,
431+
},
432+
{
433+
name: undefined,
434+
},
435+
],
436+
user: {
437+
age: undefined,
438+
name: undefined,
439+
},
440+
});
441+
});
442+
443+
test("handle notRender fields when reading and setting values", () => {
444+
const store = new FormStore();
445+
446+
store.setField("visible-field", {
447+
name: "visible-field",
448+
validate: {
449+
required: true,
450+
},
451+
});
452+
store.setField("hidden-field", {
453+
name: "hidden-field",
454+
notRender: true,
455+
validate: {
456+
required: true,
457+
},
458+
});
459+
460+
store.setInitValue({
461+
"visible-field": "visible",
462+
"hidden-field": "hidden",
463+
});
464+
465+
// getFieldsValue
466+
expect(store.getFieldsValue("visible-field")).toBe("visible");
467+
expect(store.getFieldsValue("hidden-field")).toBeUndefined();
468+
469+
// getAllValues
470+
expect(store.getAllValues()).toEqual({ "visible-field": "visible" });
471+
472+
// setFieldsValueByInitData
473+
store.setInitValue({
474+
"visible-field": "init-visible",
475+
"hidden-field": "init-hidden",
476+
});
477+
478+
store.resetFields("visible-field");
479+
expect(store.getFieldsValue("visible-field")).toBeUndefined();
480+
481+
store.setFieldsValueByInitData("visible-field");
482+
store.setFieldsValueByInitData("hidden-field");
483+
484+
expect(store.getFieldsValue("visible-field")).toBe("init-visible");
485+
expect(store.getFieldsValue("hidden-field")).toBeUndefined();
486+
487+
// validateField
488+
const validateResultVisible = store.validateField("visible-field");
489+
expect(validateResultVisible).toBeDefined();
490+
491+
const validateResultHidden = store.validateField("hidden-field");
492+
expect(validateResultHidden).toBeUndefined();
493+
});
337494
});

shared/form/src/FormStore.ts

Lines changed: 50 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { isEmpty, isNil } from "lodash";
1+
import { isEmpty, get, has, set, unset } from "lodash";
22
import { PubSub } from "./PubSub.js";
33

44
interface FormStoreOptions {
@@ -74,6 +74,7 @@ export class FormStore extends PubSub {
7474

7575
setField(name: string, detail: FieldDetail) {
7676
this.#fields.set(name, new Field(name, detail));
77+
this.setFieldsValueByInitData(name);
7778
}
7879
#getAllFields() {
7980
return [...this.#fields.keys()].filter((key) => {
@@ -83,16 +84,13 @@ export class FormStore extends PubSub {
8384

8485
getAllValues() {
8586
const allFields = this.#getAllFields();
86-
const formData = Object.fromEntries(
87-
Object.entries(this.#formData)
88-
.map(([k, v]) => {
89-
if (allFields.includes(k)) {
90-
return [k, v];
91-
}
92-
return [];
93-
})
94-
.filter((item) => item.length)
95-
);
87+
const formData: Record<string, unknown> = {};
88+
89+
allFields.forEach((fieldName) => {
90+
const value = get(this.#formData, fieldName);
91+
set(formData, fieldName, value);
92+
});
93+
9694
return formData;
9795
}
9896

@@ -102,35 +100,43 @@ export class FormStore extends PubSub {
102100
}
103101

104102
setFieldsValueByInitData(name: string) {
105-
const value = this.#initData?.[name];
106-
if (!isNil(value)) {
107-
this.#formData[name] = value;
103+
const field = this.#fields.get(name);
104+
if (field && !field.detail?.notRender && has(this.#initData, name)) {
105+
const value = get(this.#initData, name);
106+
set(this.#formData, name, value);
108107
this.publish(`${name}.init.value`, value);
109108
}
110109
}
111110

112111
setFieldsValue(values: Record<string, unknown>, isEmitValuseChange = true) {
113-
const newFormData: Record<string, unknown> = {
114-
...this.#formData,
115-
};
116-
Object.entries(values).forEach(([k, v]) => {
117-
newFormData[k] = v;
118-
this.#initData && (this.#initData[k] = v);
119-
this.publish(`${k}.init.value`, v);
112+
const newFormData = { ...this.#formData };
113+
const changedValues: Record<string, unknown> = {};
114+
115+
this.#fields.forEach((field, key) => {
116+
if (!field.detail?.notRender && has(values, key)) {
117+
const v = get(values, key);
118+
set(newFormData, key, v);
119+
if (this.#initData) {
120+
set(this.#initData, key, v);
121+
}
122+
this.publish(`${key}.init.value`, v);
123+
set(changedValues, key, v);
124+
}
120125
});
126+
121127
this.#formData = newFormData;
122128

123129
if (isEmitValuseChange) {
124130
this.#options?.onValuesChanged?.({
125-
changedValues: values,
131+
changedValues,
126132
allValues: this.getAllValues(),
127133
});
128134
}
129135
}
130136

131137
resetFields(name?: string) {
132138
if (name) {
133-
delete this.#formData[name];
139+
unset(this.#formData, name);
134140
this.publish(`${name}.reset.fields`, null);
135141
} else {
136142
this.#formData = {};
@@ -141,7 +147,11 @@ export class FormStore extends PubSub {
141147

142148
getFieldsValue(name?: string) {
143149
if (name) {
144-
return this.#formData[name];
150+
const field = this.#fields.get(name);
151+
if (field && !field.detail?.notRender) {
152+
return get(this.#formData, name);
153+
}
154+
return undefined;
145155
}
146156
return this.getAllValues();
147157
}
@@ -176,11 +186,21 @@ export class FormStore extends PubSub {
176186
}
177187

178188
validateField(field: string | FieldDetail) {
179-
const fieldDetail =
180-
typeof field === "string" ? this.#fields.get(field)?.detail : field;
181-
if (!fieldDetail) return;
189+
let fieldDetail;
190+
let fieldObj;
191+
192+
if (typeof field === "string") {
193+
fieldObj = this.#fields.get(field);
194+
fieldDetail = fieldObj?.detail;
195+
} else {
196+
fieldDetail = field;
197+
fieldObj = this.#fields.get(fieldDetail.name);
198+
}
199+
200+
if (!fieldDetail || (fieldObj && fieldObj.detail?.notRender)) return;
201+
182202
const { name, label, validate } = fieldDetail;
183-
const validateValue = this.#formData[name];
203+
const validateValue = get(this.#formData, name);
184204

185205
const messageBody = (message: string, type = "error") => {
186206
return {
@@ -193,7 +213,7 @@ export class FormStore extends PubSub {
193213

194214
const valid = (
195215
validate: Validate,
196-
value: string | number | null | undefined
216+
value: string | number | null | undefined | object | Array<unknown>
197217
): MessageBody => {
198218
const { required, pattern, message, type, min, max, validator } =
199219
validate;
@@ -271,7 +291,7 @@ export class FormStore extends PubSub {
271291
return messageBody("", "normal");
272292
};
273293

274-
const result = { name, ...valid(validate, validateValue as string) };
294+
const result = { name, ...valid(validate, validateValue as any) };
275295
this.publish(`${name}.validate`, result);
276296
return result;
277297
}

0 commit comments

Comments
 (0)