@flowgram-vue/form 0.2.0
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/LICENSE +22 -0
- package/dist/index.cjs +0 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +0 -0
- package/dist/index.js.map +1 -0
- package/package.json +65 -0
- package/src/composables/index.ts +13 -0
- package/src/composables/use-current-field-state.ts +29 -0
- package/src/composables/use-current-field.ts +31 -0
- package/src/composables/use-field-validate.ts +25 -0
- package/src/composables/use-field.ts +47 -0
- package/src/composables/use-form-model.ts +17 -0
- package/src/composables/use-form-state.ts +25 -0
- package/src/composables/use-form.ts +16 -0
- package/src/composables/use-watch.ts +34 -0
- package/src/constants.ts +34 -0
- package/src/core/create-form.ts +58 -0
- package/src/core/field-array-model.ts +333 -0
- package/src/core/field-model.ts +396 -0
- package/src/core/form-model.ts +362 -0
- package/src/core/index.ts +14 -0
- package/src/core/path.ts +125 -0
- package/src/core/store.ts +33 -0
- package/src/core/to-field-array.ts +52 -0
- package/src/core/to-field.ts +80 -0
- package/src/core/to-form.ts +64 -0
- package/src/core/utils.ts +123 -0
- package/src/env.d.ts +10 -0
- package/src/index.ts +30 -0
- package/src/types/common.ts +6 -0
- package/src/types/field.ts +193 -0
- package/src/types/form.ts +145 -0
- package/src/types/index.ts +8 -0
- package/src/types/validate.ts +88 -0
- package/src/utils/dom.ts +31 -0
- package/src/utils/event.ts +32 -0
- package/src/utils/glob.ts +279 -0
- package/src/utils/index.ts +8 -0
- package/src/utils/object.ts +107 -0
- package/src/utils/validate.ts +42 -0
- package/src/vue/context.ts +12 -0
- package/src/vue/field-array.vue +117 -0
- package/src/vue/field.vue +127 -0
- package/src/vue/form.vue +61 -0
- package/src/vue/index.ts +11 -0
- package/src/vue/types.ts +45 -0
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { flatten, get } from 'lodash-es';
|
|
7
|
+
import { deepEqual } from 'fast-equals';
|
|
8
|
+
import { Disposable, Emitter } from '@flowgram-vue/utils';
|
|
9
|
+
import { ReactiveState } from '@flowgram-vue/reactive';
|
|
10
|
+
|
|
11
|
+
import { feedbackToFieldErrorsOrWarnings, hasError, toFeedback } from '../utils/validate';
|
|
12
|
+
import { Glob } from '../utils/glob';
|
|
13
|
+
import { keepValidKeys } from '../utils';
|
|
14
|
+
import {
|
|
15
|
+
FormModelState,
|
|
16
|
+
FormOptions,
|
|
17
|
+
FormState,
|
|
18
|
+
OnFormValuesChangePayload,
|
|
19
|
+
OnFormValuesInitPayload,
|
|
20
|
+
OnFormValuesUpdatedPayload,
|
|
21
|
+
} from '../types/form';
|
|
22
|
+
import { FieldName, FieldValue } from '../types/field';
|
|
23
|
+
import { Errors, FeedbackLevel, FormValidateReturn, Validate, Warnings } from '../types';
|
|
24
|
+
import { createFormModelState } from '../constants';
|
|
25
|
+
import { getValidByErrors, mergeFeedbacks } from './utils';
|
|
26
|
+
import { Store } from './store';
|
|
27
|
+
import { Path } from './path';
|
|
28
|
+
import { FieldModel } from './field-model';
|
|
29
|
+
import { FieldArrayModel } from './field-array-model';
|
|
30
|
+
|
|
31
|
+
export class FormModel<TValues = any> implements Disposable {
|
|
32
|
+
protected _fieldMap: Map<string, FieldModel> = new Map();
|
|
33
|
+
|
|
34
|
+
readonly store = new Store();
|
|
35
|
+
|
|
36
|
+
protected _options: FormOptions = {};
|
|
37
|
+
|
|
38
|
+
protected onFieldModelCreateEmitter = new Emitter<FieldModel>();
|
|
39
|
+
|
|
40
|
+
readonly onFieldModelCreate = this.onFieldModelCreateEmitter.event;
|
|
41
|
+
|
|
42
|
+
readonly onFormValuesChangeEmitter = new Emitter<OnFormValuesChangePayload>();
|
|
43
|
+
|
|
44
|
+
readonly onFormValuesChange = this.onFormValuesChangeEmitter.event;
|
|
45
|
+
|
|
46
|
+
readonly onFormValuesInitEmitter = new Emitter<OnFormValuesInitPayload>();
|
|
47
|
+
|
|
48
|
+
readonly onFormValuesInit = this.onFormValuesInitEmitter.event;
|
|
49
|
+
|
|
50
|
+
readonly onFormValuesUpdatedEmitter = new Emitter<OnFormValuesUpdatedPayload>();
|
|
51
|
+
|
|
52
|
+
readonly onFormValuesUpdated = this.onFormValuesUpdatedEmitter.event;
|
|
53
|
+
|
|
54
|
+
readonly onValidateEmitter = new Emitter<FormModelState>();
|
|
55
|
+
|
|
56
|
+
readonly onValidate = this.onValidateEmitter.event;
|
|
57
|
+
|
|
58
|
+
protected _state: ReactiveState<FormModelState> = new ReactiveState<FormModelState>(
|
|
59
|
+
createFormModelState()
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
protected _initialized = false;
|
|
63
|
+
|
|
64
|
+
set fieldMap(map) {
|
|
65
|
+
this._fieldMap = map;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* 表单初始值,初始化设置后不可修改
|
|
70
|
+
* @protected
|
|
71
|
+
*/
|
|
72
|
+
// protected _initialValues?: TValues;
|
|
73
|
+
|
|
74
|
+
get fieldMap() {
|
|
75
|
+
return this._fieldMap;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
get context() {
|
|
79
|
+
return this._options.context;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
get initialValues() {
|
|
83
|
+
return this._options.initialValues;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
get values() {
|
|
87
|
+
return this.store.values;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
set values(v) {
|
|
91
|
+
const prevValues = this.values;
|
|
92
|
+
if (deepEqual(prevValues, v)) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
this.store.values = v;
|
|
96
|
+
this.fireOnFormValuesChange({
|
|
97
|
+
values: this.values,
|
|
98
|
+
prevValues,
|
|
99
|
+
name: '',
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
get validationTrigger() {
|
|
104
|
+
return this._options.validateTrigger;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
get state() {
|
|
108
|
+
return this._state.value;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
get reactiveState() {
|
|
112
|
+
return this._state;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
get fields(): FieldModel[] {
|
|
116
|
+
return Array.from(this.fieldMap.values());
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
updateState(state: Partial<FormState>) {
|
|
120
|
+
// todo
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
get initialized() {
|
|
124
|
+
return this._initialized;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
fireOnFormValuesChange(payload: OnFormValuesChangePayload) {
|
|
128
|
+
this.onFormValuesChangeEmitter.fire(payload);
|
|
129
|
+
this.onFormValuesUpdatedEmitter.fire(payload);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
fireOnFormValuesInit(payload: OnFormValuesInitPayload) {
|
|
133
|
+
this.onFormValuesInitEmitter.fire(payload);
|
|
134
|
+
this.onFormValuesUpdatedEmitter.fire(payload);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
init(options: FormOptions<TValues>) {
|
|
138
|
+
this._options = options;
|
|
139
|
+
if (options.initialValues) {
|
|
140
|
+
const prevValues = this.store.values;
|
|
141
|
+
this.store.values = options.initialValues;
|
|
142
|
+
this.fireOnFormValuesInit({
|
|
143
|
+
values: options.initialValues,
|
|
144
|
+
prevValues,
|
|
145
|
+
name: '',
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
this._initialized = true;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
createField<TValue = FieldValue>(name: FieldName, isArray?: boolean): FieldModel<TValue> {
|
|
152
|
+
const path = new Path(name);
|
|
153
|
+
const pathString = path.toString();
|
|
154
|
+
|
|
155
|
+
if (this.fieldMap.get(pathString)) {
|
|
156
|
+
return this.fieldMap.get(pathString)!;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// const fieldValue = value || get(this.initialValues, pathString);
|
|
160
|
+
|
|
161
|
+
const field: FieldModel = isArray
|
|
162
|
+
? new FieldArrayModel(path, this)
|
|
163
|
+
: new FieldModel(path, this);
|
|
164
|
+
|
|
165
|
+
this.fieldMap.set(pathString, field);
|
|
166
|
+
field.onDispose(() => {
|
|
167
|
+
this.fieldMap.delete(pathString);
|
|
168
|
+
});
|
|
169
|
+
this.onFieldModelCreateEmitter.fire(field);
|
|
170
|
+
|
|
171
|
+
return field;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
createFieldArray<TValue = FieldValue>(
|
|
175
|
+
name: FieldName,
|
|
176
|
+
value?: Array<TValue>
|
|
177
|
+
): FieldArrayModel<TValue> {
|
|
178
|
+
return this.createField<Array<TValue>>(name, true) as FieldArrayModel<TValue>;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* 销毁Field 模型和子模型,但不会删除field的值
|
|
183
|
+
* @param name
|
|
184
|
+
*/
|
|
185
|
+
disposeField(name: string) {
|
|
186
|
+
const field = this.fieldMap.get(name);
|
|
187
|
+
if (field) {
|
|
188
|
+
field.dispose();
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* 删除field, 会删除值和 Field 模型, 以及对应的子模型
|
|
194
|
+
* @param name
|
|
195
|
+
*/
|
|
196
|
+
deleteField(name: string) {
|
|
197
|
+
const field = this.fieldMap.get(name);
|
|
198
|
+
if (field) {
|
|
199
|
+
// 销毁值
|
|
200
|
+
field.clear();
|
|
201
|
+
// 销毁模型
|
|
202
|
+
field.dispose();
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
getField<TFieldModel extends FieldModel | FieldArrayModel = FieldModel>(
|
|
207
|
+
name: FieldName
|
|
208
|
+
): TFieldModel | undefined {
|
|
209
|
+
return this.fieldMap.get(new Path(name).toString()) as TFieldModel | undefined;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
getValueIn<TValue>(name: FieldName): TValue {
|
|
213
|
+
return this.store.getIn<TValue>(new Path(name));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
setValueIn<TValue>(name: FieldName, value: TValue): void {
|
|
217
|
+
const prevValues = this.values;
|
|
218
|
+
|
|
219
|
+
this.store.setIn(new Path(name), value);
|
|
220
|
+
|
|
221
|
+
this.fireOnFormValuesChange({
|
|
222
|
+
values: this.values,
|
|
223
|
+
prevValues,
|
|
224
|
+
name,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
setInitValueIn<TValue = any>(name: FieldName, value: TValue): void {
|
|
229
|
+
const path = new Path(name);
|
|
230
|
+
const prevValue = this.store.getIn(path);
|
|
231
|
+
if (prevValue === undefined) {
|
|
232
|
+
const prevValues = this.values;
|
|
233
|
+
this.store.setIn(new Path(name), value);
|
|
234
|
+
this.fireOnFormValuesInit({
|
|
235
|
+
values: this.values,
|
|
236
|
+
prevValues,
|
|
237
|
+
name,
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
validateDisabled = false;
|
|
243
|
+
|
|
244
|
+
clearValueIn(name: FieldName) {
|
|
245
|
+
this.setValueIn(name, undefined);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async validateIn(name: FieldName) {
|
|
249
|
+
if (this.validateDisabled) return [];
|
|
250
|
+
const validateOptions = this.getValidateOptions();
|
|
251
|
+
if (!validateOptions) {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const validateKeys = Object.keys(validateOptions).filter((pattern) =>
|
|
256
|
+
Glob.isMatch(pattern, name)
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
const validatePromises = validateKeys.map(async (validateKey) => {
|
|
260
|
+
const validate = validateOptions![validateKey];
|
|
261
|
+
|
|
262
|
+
return validate({
|
|
263
|
+
value: this.getValueIn(name),
|
|
264
|
+
formValues: this.values,
|
|
265
|
+
context: this.context,
|
|
266
|
+
name,
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
return Promise.all(validatePromises);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
protected getValidateOptions(): Record<string, Validate> | undefined {
|
|
274
|
+
const validate = this._options.validate;
|
|
275
|
+
if (typeof validate === 'function') {
|
|
276
|
+
return validate(this.values, this.context);
|
|
277
|
+
}
|
|
278
|
+
return validate;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async validate(): Promise<FormValidateReturn> {
|
|
282
|
+
if (this.validateDisabled) return [];
|
|
283
|
+
const validateOptions = this.getValidateOptions();
|
|
284
|
+
if (!validateOptions) {
|
|
285
|
+
return [];
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const feedbacksArrPromises = Object.keys(validateOptions).map(async (nameRule) => {
|
|
289
|
+
const validate = validateOptions![nameRule];
|
|
290
|
+
const values = this.values;
|
|
291
|
+
const paths = Glob.findMatchPathsWithEmptyValue(values, nameRule);
|
|
292
|
+
return Promise.all(
|
|
293
|
+
paths.map(async (path) => {
|
|
294
|
+
const result = await validate({
|
|
295
|
+
value: get(values, path),
|
|
296
|
+
formValues: values,
|
|
297
|
+
context: this.context,
|
|
298
|
+
name: path,
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
const feedback = toFeedback(result, path);
|
|
302
|
+
const field = this.getField(path);
|
|
303
|
+
|
|
304
|
+
const errors = feedbackToFieldErrorsOrWarnings<Errors>(
|
|
305
|
+
path,
|
|
306
|
+
feedback?.level === FeedbackLevel.Error ? feedback : undefined
|
|
307
|
+
);
|
|
308
|
+
const warnings = feedbackToFieldErrorsOrWarnings<Warnings>(
|
|
309
|
+
path,
|
|
310
|
+
feedback?.level === FeedbackLevel.Warning ? feedback : undefined
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
if (field) {
|
|
314
|
+
field.state.errors = errors;
|
|
315
|
+
field.state.warnings = warnings;
|
|
316
|
+
field.state.invalid = hasError(errors);
|
|
317
|
+
field.bubbleState();
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// 无论是否存在 field 都要保证 form 的state 被更新
|
|
321
|
+
this.state.errors = mergeFeedbacks(this.state.errors, errors);
|
|
322
|
+
this.state.warnings = mergeFeedbacks(this.state.warnings, warnings);
|
|
323
|
+
|
|
324
|
+
this.state.invalid = !getValidByErrors(this.state.errors);
|
|
325
|
+
return feedback;
|
|
326
|
+
})
|
|
327
|
+
);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
this.state.isValidating = true;
|
|
331
|
+
const feedbacksArr = await Promise.all(feedbacksArrPromises);
|
|
332
|
+
this.state.isValidating = false;
|
|
333
|
+
this.onValidateEmitter.fire(this.state);
|
|
334
|
+
|
|
335
|
+
return flatten(feedbacksArr).filter(Boolean) as FormValidateReturn;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
alignStateWithFieldMap() {
|
|
339
|
+
const keys = Array.from(this.fieldMap.keys());
|
|
340
|
+
|
|
341
|
+
if (this.state.errors) {
|
|
342
|
+
this.state.errors = keepValidKeys(this.state.errors, keys);
|
|
343
|
+
}
|
|
344
|
+
if (this.state.warnings) {
|
|
345
|
+
this.state.warnings = keepValidKeys(this.state.warnings, keys);
|
|
346
|
+
}
|
|
347
|
+
this.fieldMap.forEach((f) => {
|
|
348
|
+
if (f.state.errors) {
|
|
349
|
+
f.state.errors = keepValidKeys(f.state.errors, keys);
|
|
350
|
+
}
|
|
351
|
+
if (f.state.warnings) {
|
|
352
|
+
f.state.warnings = keepValidKeys(f.state.warnings, keys);
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
dispose() {
|
|
358
|
+
this.fieldMap.forEach((f) => f.dispose());
|
|
359
|
+
this.store.dispose();
|
|
360
|
+
this._initialized = false;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export { FormModel } from './form-model';
|
|
7
|
+
export { createForm, type CreateFormOptions } from './create-form';
|
|
8
|
+
export { FieldModel } from './field-model';
|
|
9
|
+
export { FieldArrayModel } from './field-array-model';
|
|
10
|
+
|
|
11
|
+
export { toField, toFieldState } from './to-field';
|
|
12
|
+
export { toFieldArray } from './to-field-array';
|
|
13
|
+
export { toForm, toFormState } from './to-form';
|
|
14
|
+
export { Path } from './path';
|
package/src/core/path.ts
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { toPath } from 'lodash-es';
|
|
7
|
+
|
|
8
|
+
export class Path {
|
|
9
|
+
protected _path: string[] = [];
|
|
10
|
+
|
|
11
|
+
constructor(path: string | string[]) {
|
|
12
|
+
this._path = toPath(path);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
get parent(): Path | undefined {
|
|
16
|
+
if (this._path.length < 2) {
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
return new Path(this._path.slice(0, -1));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
toString(): string {
|
|
23
|
+
return this._path.join('.');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
get value(): string[] {
|
|
27
|
+
return this._path;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 仅计直系child
|
|
32
|
+
* @param path
|
|
33
|
+
*/
|
|
34
|
+
isChild(path: string) {
|
|
35
|
+
const target = new Path(path).value;
|
|
36
|
+
const self = this.value;
|
|
37
|
+
|
|
38
|
+
if (target.length - self.length !== 1) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
for (let i = 0; i < self.length; i++) {
|
|
43
|
+
if (target[i] !== self[i]) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 比较两个数组path大小
|
|
52
|
+
* 返回小于0则path1<path2, 大于0 则path1>path2, 等于0则相等
|
|
53
|
+
* @param path1
|
|
54
|
+
* @param path2
|
|
55
|
+
*/
|
|
56
|
+
static compareArrayPath(path1: Path, path2: Path): number | void {
|
|
57
|
+
let i = 0;
|
|
58
|
+
while (path1.value[i] && path2.value[i]) {
|
|
59
|
+
const index1 = parseInt(path1.value[i]);
|
|
60
|
+
const index2 = parseInt(path2.value[i]);
|
|
61
|
+
|
|
62
|
+
if (!isNaN(index1) && !isNaN(index2)) {
|
|
63
|
+
return index1 - index2;
|
|
64
|
+
} else if (path1.value[i] !== path2.value[i]) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
`[Form] Path.compareArrayPath invalid input Error: two path should refers to the same array, but got path1: ${path1.toString()}, path2: ${path2.toString()}`
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
i++;
|
|
70
|
+
}
|
|
71
|
+
throw new Error(
|
|
72
|
+
`[Form] Path.compareArrayPath invalid input Error: got path1: ${path1.toString()}, path2: ${path2.toString()}`
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
isChildOrGrandChild(path: string) {
|
|
77
|
+
const target = new Path(path).value;
|
|
78
|
+
const self = this.value;
|
|
79
|
+
|
|
80
|
+
if (target.length - self.length < 1) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
for (let i = 0; i < self.length; i++) {
|
|
85
|
+
if (target[i] !== self[i]) {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
getArrayIndex(parent: Path) {
|
|
93
|
+
return parseInt(this._path[parent.value.length]);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
concat(name: number | string) {
|
|
97
|
+
if (typeof name === 'string' || typeof name === 'number') {
|
|
98
|
+
return new Path(this._path.concat(new Path(name.toString())._path));
|
|
99
|
+
}
|
|
100
|
+
throw new Error(
|
|
101
|
+
`[Form] Error in Path.concat: invalid param type, require number or string, but got ${typeof name}`
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
replaceParent(parent: Path, newParent: Path) {
|
|
106
|
+
if (parent.value.length > this.value.length) {
|
|
107
|
+
throw new Error(
|
|
108
|
+
`[Form] Error in Path.replaceParent: invalid parent param: ${parent}, parent length should not greater than current length.`
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
const rest = [];
|
|
112
|
+
for (let i = 0; i < this.value.length; i++) {
|
|
113
|
+
if (i < parent.value.length && parent.value[i] !== this.value[i]) {
|
|
114
|
+
throw new Error(
|
|
115
|
+
`[Form] Error in Path.replaceParent: invalid parent param: '${parent}' is not a parent of '${this.toString()}'`
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
if (i >= parent.value.length) {
|
|
119
|
+
rest.push(this.value[i]);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return new Path(newParent.value.concat(rest));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { get, clone, cloneDeep } from 'lodash-es';
|
|
7
|
+
|
|
8
|
+
import { shallowSetIn } from '../utils';
|
|
9
|
+
import { FieldValue } from '../types/field';
|
|
10
|
+
import { Path } from './path';
|
|
11
|
+
|
|
12
|
+
export class Store<TValues = FieldValue> {
|
|
13
|
+
protected _values: TValues;
|
|
14
|
+
|
|
15
|
+
get values(): TValues {
|
|
16
|
+
return clone(this._values);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
set values(v) {
|
|
20
|
+
this._values = cloneDeep(v);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
setIn<TValue = FieldValue>(path: Path, value: TValue): void {
|
|
24
|
+
// shallow clone set
|
|
25
|
+
this._values = shallowSetIn(this._values || {}, path.toString(), value);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
getIn<TValue = FieldValue>(path: Path): TValue {
|
|
29
|
+
return get(this.values, path.value);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
dispose() {}
|
|
33
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Field, FieldArray } from '../types/field';
|
|
7
|
+
import { toField } from './to-field';
|
|
8
|
+
import { FieldArrayModel } from './field-array-model';
|
|
9
|
+
|
|
10
|
+
export function toFieldArray<TValue>(model: FieldArrayModel<TValue>): FieldArray<TValue> {
|
|
11
|
+
const res: FieldArray<TValue> = {
|
|
12
|
+
get key() {
|
|
13
|
+
return model.id;
|
|
14
|
+
},
|
|
15
|
+
get name() {
|
|
16
|
+
return model.path.toString();
|
|
17
|
+
},
|
|
18
|
+
get value() {
|
|
19
|
+
return model.value;
|
|
20
|
+
},
|
|
21
|
+
onChange: (value) => {
|
|
22
|
+
model.value = value;
|
|
23
|
+
},
|
|
24
|
+
map: <T = any>(cb: (f: Field<TValue>, index: number) => T) =>
|
|
25
|
+
model.map<T>((f, index) => cb(toField(f), index)),
|
|
26
|
+
append: (value) => toField<TValue>(model.append(value)),
|
|
27
|
+
/**
|
|
28
|
+
* @deprecated: use remove instead
|
|
29
|
+
* @param index
|
|
30
|
+
*/
|
|
31
|
+
delete: (index: number) => model.delete(index),
|
|
32
|
+
remove: (index: number) => model.delete(index),
|
|
33
|
+
swap: (from: number, to: number) => model.swap(from, to),
|
|
34
|
+
move: (from: number, to: number) => model.move(from, to),
|
|
35
|
+
} as FieldArray<TValue>;
|
|
36
|
+
|
|
37
|
+
// Object.defineProperty(res, 'validate', {
|
|
38
|
+
// enumerable: false,
|
|
39
|
+
// get() {
|
|
40
|
+
// return model.validate.bind(model);
|
|
41
|
+
// },
|
|
42
|
+
// });
|
|
43
|
+
|
|
44
|
+
// 隐藏属性
|
|
45
|
+
Object.defineProperty(res, '_fieldModel', {
|
|
46
|
+
enumerable: false,
|
|
47
|
+
get() {
|
|
48
|
+
return model;
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
return res;
|
|
52
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { isNativeChangeEvent } from '../utils';
|
|
7
|
+
import { Field, FieldModelState } from '../types/field';
|
|
8
|
+
import { ValidateTrigger } from '../types';
|
|
9
|
+
import { shouldValidate } from './utils';
|
|
10
|
+
import { FieldModel } from './field-model';
|
|
11
|
+
|
|
12
|
+
export function toField<TValue>(model: FieldModel): Field<TValue> {
|
|
13
|
+
const res: Field<TValue> = {
|
|
14
|
+
get name() {
|
|
15
|
+
return model.name;
|
|
16
|
+
},
|
|
17
|
+
get value() {
|
|
18
|
+
return model.value;
|
|
19
|
+
},
|
|
20
|
+
onChange: (e: unknown) => {
|
|
21
|
+
if (isNativeChangeEvent(e)) {
|
|
22
|
+
model.value = e.target.type === 'checkbox' ? e.target.checked : e.target.value;
|
|
23
|
+
} else {
|
|
24
|
+
model.value = e;
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
onBlur() {
|
|
28
|
+
if (shouldValidate(ValidateTrigger.onBlur, model.form.validationTrigger)) {
|
|
29
|
+
model.validate();
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
onFocus() {
|
|
33
|
+
model.state.isTouched = true;
|
|
34
|
+
},
|
|
35
|
+
} as Field<TValue>;
|
|
36
|
+
|
|
37
|
+
Object.defineProperty(res, 'key', {
|
|
38
|
+
enumerable: false,
|
|
39
|
+
get() {
|
|
40
|
+
return model.id;
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
Object.defineProperty(res, '_fieldModel', {
|
|
45
|
+
enumerable: false,
|
|
46
|
+
get() {
|
|
47
|
+
return model;
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
return res;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function toFieldState(modelState: FieldModelState) {
|
|
54
|
+
return {
|
|
55
|
+
get isTouched() {
|
|
56
|
+
return modelState.isTouched;
|
|
57
|
+
},
|
|
58
|
+
get invalid() {
|
|
59
|
+
return modelState.invalid;
|
|
60
|
+
},
|
|
61
|
+
get isDirty() {
|
|
62
|
+
return modelState.isDirty;
|
|
63
|
+
},
|
|
64
|
+
get isValidating() {
|
|
65
|
+
return modelState.isValidating;
|
|
66
|
+
},
|
|
67
|
+
get errors() {
|
|
68
|
+
if (modelState.errors) {
|
|
69
|
+
return Object.values(modelState.errors).reduce((acc, arr) => acc.concat(arr), []);
|
|
70
|
+
}
|
|
71
|
+
return;
|
|
72
|
+
},
|
|
73
|
+
get warnings() {
|
|
74
|
+
if (modelState.warnings) {
|
|
75
|
+
return Object.values(modelState.warnings).reduce((acc, arr) => acc.concat(arr), []);
|
|
76
|
+
}
|
|
77
|
+
return;
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|