@tmagic/form 1.8.0-beta.16 → 1.8.0-beta.18
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/es/style.css +5 -1
- package/dist/es/submitForm.js +73 -52
- package/dist/es/utils/form.js +4 -3
- package/dist/es/utils/typeMatch.js +98 -21
- package/dist/style.css +5 -1
- package/dist/themes/magic-admin.css +11 -1
- package/dist/tmagic-form.umd.cjs +175 -76
- package/package.json +4 -4
- package/src/submitForm.ts +131 -78
- package/src/theme/group-list.scss +11 -2
- package/src/theme/themes/magic-admin/index.scss +11 -0
- package/src/utils/form.ts +7 -4
- package/src/utils/typeMatch.ts +135 -31
- package/types/index.d.ts +22 -3
package/dist/es/style.css
CHANGED
|
@@ -296,12 +296,16 @@ fieldset.m-fieldset .m-form-tip {
|
|
|
296
296
|
gap: 8px;
|
|
297
297
|
}
|
|
298
298
|
.m-fields-group-list .m-fields-group-list-footer {
|
|
299
|
-
display: flex;
|
|
300
299
|
justify-content: space-between;
|
|
301
300
|
margin-top: 10px;
|
|
302
301
|
margin-bottom: 16px;
|
|
303
302
|
}
|
|
304
303
|
|
|
304
|
+
/** 最外层的groupList需要每个item需要增加空白区域界限时,增加outer-gorup_list可以实现 */
|
|
305
|
+
.m-container-group-list.outer-gorup_list > .m-fields-group-list > .m-fields-group-list-item {
|
|
306
|
+
margin-bottom: 10px;
|
|
307
|
+
}
|
|
308
|
+
|
|
305
309
|
.m-form-panel .el-card__header:hover {
|
|
306
310
|
background: #f2f6fc;
|
|
307
311
|
}
|
package/dist/es/submitForm.js
CHANGED
|
@@ -3,6 +3,8 @@ import { applyExtendState } from "./utils/form.js";
|
|
|
3
3
|
import Form_default from "./Form.js";
|
|
4
4
|
import { createApp, defineComponent, h, nextTick, provide, ref, watch } from "vue";
|
|
5
5
|
//#region packages/form/src/submitForm.ts
|
|
6
|
+
/** 未指定或传入非正数 timeout 时的兜底超时(毫秒),保证非 debug 挂载始终能被清理 */
|
|
7
|
+
var DEFAULT_MOUNT_TIMEOUT = 1e4;
|
|
6
8
|
/**
|
|
7
9
|
* submitForm / validateForm 的公共脚手架:
|
|
8
10
|
*
|
|
@@ -13,14 +15,19 @@ import { createApp, defineComponent, h, nextTick, provide, ref, watch } from "vu
|
|
|
13
15
|
* 容器创建、卸载、超时、上下文注入等模板代码在此统一收口。
|
|
14
16
|
*/
|
|
15
17
|
var mountFormInstance = (options) => {
|
|
16
|
-
const { formProps, appContext, timeout, timeoutMessage, hidden = true, skipTimeout = false, createWrapper } = options;
|
|
18
|
+
const { formProps, appContext, timeout, timeoutMessage, hidden = true, skipTimeout = false, signal, createWrapper } = options;
|
|
17
19
|
return new Promise((resolve, reject) => {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
if (signal?.aborted) {
|
|
21
|
+
reject(signal.reason ?? /* @__PURE__ */ new Error("mountFormInstance aborted"));
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
21
24
|
let cleaned = false;
|
|
22
25
|
let timer = null;
|
|
26
|
+
let onAbort = null;
|
|
23
27
|
const instance = { app: null };
|
|
28
|
+
const container = document.createElement("div");
|
|
29
|
+
if (hidden) container.style.display = "none";
|
|
30
|
+
document.body.appendChild(container);
|
|
24
31
|
const cleanup = () => {
|
|
25
32
|
if (cleaned) return;
|
|
26
33
|
cleaned = true;
|
|
@@ -28,59 +35,71 @@ var mountFormInstance = (options) => {
|
|
|
28
35
|
clearTimeout(timer);
|
|
29
36
|
timer = null;
|
|
30
37
|
}
|
|
38
|
+
if (signal && onAbort) {
|
|
39
|
+
signal.removeEventListener("abort", onAbort);
|
|
40
|
+
onAbort = null;
|
|
41
|
+
}
|
|
31
42
|
try {
|
|
32
43
|
instance.app?.unmount();
|
|
33
44
|
} catch {}
|
|
34
45
|
container.parentNode?.removeChild(container);
|
|
35
46
|
};
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
formProps: restFormProps,
|
|
41
|
-
cleanup,
|
|
42
|
-
resolve,
|
|
43
|
-
reject
|
|
44
|
-
});
|
|
45
|
-
const wrapperComponent = typeof extendState === "function" ? defineComponent({
|
|
46
|
-
name: "MFormExtendStateInjector",
|
|
47
|
-
setup() {
|
|
48
|
-
watch(() => formRef.value, (form) => {
|
|
49
|
-
if (!form) return;
|
|
50
|
-
let result;
|
|
51
|
-
try {
|
|
52
|
-
result = extendState(form.formState);
|
|
53
|
-
} catch (e) {
|
|
54
|
-
console.error("[MForm] extendState failed:", e);
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
|
-
const reservedStateKeys = new Set(Reflect.ownKeys(form.formState));
|
|
58
|
-
const apply = (state) => applyExtendState(form.formState, state, reservedStateKeys);
|
|
59
|
-
if (result && typeof result.then === "function") result.then(apply, (e) => console.error("[MForm] extendState failed:", e));
|
|
60
|
-
else apply(result);
|
|
61
|
-
}, {
|
|
62
|
-
flush: "sync",
|
|
63
|
-
immediate: true
|
|
64
|
-
});
|
|
65
|
-
return () => h(userWrapper);
|
|
66
|
-
}
|
|
67
|
-
}) : userWrapper;
|
|
68
|
-
const app = createApp(hidden ? defineComponent({
|
|
69
|
-
name: "MFormSilentProvider",
|
|
70
|
-
setup() {
|
|
71
|
-
provide(FORM_SILENT_MODE_KEY, true);
|
|
72
|
-
return () => h(wrapperComponent);
|
|
73
|
-
}
|
|
74
|
-
}) : wrapperComponent);
|
|
75
|
-
instance.app = app;
|
|
76
|
-
if (appContext) Object.assign(app._context, appContext);
|
|
77
|
-
if (timeout > 0 && !skipTimeout) timer = setTimeout(() => {
|
|
78
|
-
if (!cleaned) {
|
|
79
|
-
reject(new Error(timeoutMessage));
|
|
47
|
+
if (signal) {
|
|
48
|
+
onAbort = () => {
|
|
49
|
+
if (cleaned) return;
|
|
50
|
+
reject(signal.reason ?? /* @__PURE__ */ new Error("mountFormInstance aborted"));
|
|
80
51
|
cleanup();
|
|
81
|
-
}
|
|
82
|
-
|
|
52
|
+
};
|
|
53
|
+
signal.addEventListener("abort", onAbort);
|
|
54
|
+
}
|
|
83
55
|
try {
|
|
56
|
+
const formRef = ref(null);
|
|
57
|
+
const { extendState, ...restFormProps } = formProps;
|
|
58
|
+
const userWrapper = createWrapper({
|
|
59
|
+
formRef,
|
|
60
|
+
formProps: restFormProps,
|
|
61
|
+
cleanup,
|
|
62
|
+
resolve,
|
|
63
|
+
reject
|
|
64
|
+
});
|
|
65
|
+
const wrapperComponent = typeof extendState === "function" ? defineComponent({
|
|
66
|
+
name: "MFormExtendStateInjector",
|
|
67
|
+
setup() {
|
|
68
|
+
watch(() => formRef.value, (form) => {
|
|
69
|
+
if (!form) return;
|
|
70
|
+
let result;
|
|
71
|
+
try {
|
|
72
|
+
result = extendState(form.formState);
|
|
73
|
+
} catch (e) {
|
|
74
|
+
console.error("[MForm] extendState failed:", e);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const reservedStateKeys = new Set(Reflect.ownKeys(form.formState));
|
|
78
|
+
const apply = (state) => applyExtendState(form.formState, state, reservedStateKeys);
|
|
79
|
+
if (result && typeof result.then === "function") result.then(apply, (e) => console.error("[MForm] extendState failed:", e));
|
|
80
|
+
else apply(result);
|
|
81
|
+
}, {
|
|
82
|
+
flush: "sync",
|
|
83
|
+
immediate: true
|
|
84
|
+
});
|
|
85
|
+
return () => h(userWrapper);
|
|
86
|
+
}
|
|
87
|
+
}) : userWrapper;
|
|
88
|
+
const app = createApp(hidden ? defineComponent({
|
|
89
|
+
name: "MFormSilentProvider",
|
|
90
|
+
setup() {
|
|
91
|
+
provide(FORM_SILENT_MODE_KEY, true);
|
|
92
|
+
return () => h(wrapperComponent);
|
|
93
|
+
}
|
|
94
|
+
}) : wrapperComponent);
|
|
95
|
+
instance.app = app;
|
|
96
|
+
if (appContext) Object.assign(app._context, appContext);
|
|
97
|
+
if (!skipTimeout) timer = setTimeout(() => {
|
|
98
|
+
if (!cleaned) {
|
|
99
|
+
reject(new Error(timeoutMessage));
|
|
100
|
+
cleanup();
|
|
101
|
+
}
|
|
102
|
+
}, timeout > 0 ? timeout : DEFAULT_MOUNT_TIMEOUT);
|
|
84
103
|
app.mount(container);
|
|
85
104
|
} catch (err) {
|
|
86
105
|
reject(err);
|
|
@@ -213,11 +232,12 @@ var createDebugWrapper = (options) => {
|
|
|
213
232
|
* ```
|
|
214
233
|
*/
|
|
215
234
|
var submitForm = (options) => {
|
|
216
|
-
const { native, appContext, timeout = 1e4, returnChangeRecords, debug = false, ...formProps } = options;
|
|
235
|
+
const { native, appContext, timeout = 1e4, returnChangeRecords, debug = false, signal, ...formProps } = options;
|
|
217
236
|
return mountFormInstance({
|
|
218
237
|
formProps,
|
|
219
238
|
appContext,
|
|
220
239
|
timeout,
|
|
240
|
+
signal,
|
|
221
241
|
hidden: !debug,
|
|
222
242
|
skipTimeout: debug,
|
|
223
243
|
timeoutMessage: `submitForm timeout after ${timeout}ms: form is not initialized.`,
|
|
@@ -354,7 +374,7 @@ var stripTabItemsLazy = (config) => {
|
|
|
354
374
|
* ```
|
|
355
375
|
*/
|
|
356
376
|
var validateForm = (options) => {
|
|
357
|
-
const { appContext, timeout = 1e4, debug = false, config, ...rest } = options;
|
|
377
|
+
const { appContext, timeout = 1e4, debug = false, config, signal, ...rest } = options;
|
|
358
378
|
return mountFormInstance({
|
|
359
379
|
formProps: {
|
|
360
380
|
...rest,
|
|
@@ -362,6 +382,7 @@ var validateForm = (options) => {
|
|
|
362
382
|
},
|
|
363
383
|
appContext,
|
|
364
384
|
timeout,
|
|
385
|
+
signal,
|
|
365
386
|
hidden: !debug,
|
|
366
387
|
skipTimeout: debug,
|
|
367
388
|
timeoutMessage: `validateForm timeout after ${timeout}ms: form is not initialized.`,
|
package/dist/es/utils/form.js
CHANGED
|
@@ -20,15 +20,16 @@ var adaptFormValidator = (validator) => {
|
|
|
20
20
|
const callback = (error) => {
|
|
21
21
|
if (settled) return;
|
|
22
22
|
settled = true;
|
|
23
|
-
|
|
23
|
+
const first = Array.isArray(error) ? error[0] : error;
|
|
24
|
+
if (first) resolve({
|
|
24
25
|
result: false,
|
|
25
|
-
message: typeof
|
|
26
|
+
message: typeof first === "string" ? first : first.message
|
|
26
27
|
});
|
|
27
28
|
else resolve(true);
|
|
28
29
|
};
|
|
29
30
|
try {
|
|
30
31
|
const result = validator(void 0, value, callback);
|
|
31
|
-
if (result
|
|
32
|
+
if (result && typeof result.then === "function") Promise.resolve(result).then(() => {
|
|
32
33
|
if (!settled) callback();
|
|
33
34
|
}, (err) => {
|
|
34
35
|
callback(err instanceof Error ? err : new Error(String(err)));
|
|
@@ -361,6 +361,11 @@ var validateBuiltinTypeMatch = (value, fieldType, mForm, props, message) => {
|
|
|
361
361
|
return;
|
|
362
362
|
}
|
|
363
363
|
};
|
|
364
|
+
/**
|
|
365
|
+
* 校验取值与字段 type 是否匹配;通过返回 undefined,否则返回错误文案。
|
|
366
|
+
*
|
|
367
|
+
* 命中的自定义规则是异步校验器时返回 Promise,内置规则始终同步返回。
|
|
368
|
+
*/
|
|
364
369
|
var validateTypeMatch = (value, mForm, props, message) => {
|
|
365
370
|
if (isEmptyValue(value) || isEmptyArray(value)) return;
|
|
366
371
|
if (!props.config?.name) return;
|
|
@@ -376,34 +381,106 @@ var validateTypeMatch = (value, mForm, props, message) => {
|
|
|
376
381
|
});
|
|
377
382
|
return validateBuiltinTypeMatch(value, fieldType, mForm, props, message);
|
|
378
383
|
};
|
|
384
|
+
var toError = (error) => error instanceof Error ? error : /* @__PURE__ */ new Error(`${error}`);
|
|
379
385
|
var createTypeMatchValidator = (mForm, props, rule) => {
|
|
380
386
|
const originalValidator = typeof rule.validator === "function" ? rule.validator : void 0;
|
|
387
|
+
/**
|
|
388
|
+
* 每次校验取一个自增序号,只有最新一轮的结论会被采用。
|
|
389
|
+
*
|
|
390
|
+
* 旧值的在途校验不能用自己的结论结算:它可能晚于新校验结束,用过期结论覆盖新结论
|
|
391
|
+
* (表单取最后结束的那次结果);也不能在新校验开始时无条件按通过结算,否则
|
|
392
|
+
* `form.validate()` 会对一个还没校验过的值返回成功。因此所有尚未结算的调用都登记在
|
|
393
|
+
* pendingSettlers 中,等最新一轮出结论后用同一个结论一起结算。
|
|
394
|
+
*
|
|
395
|
+
* 不比较取值本身:配了 names 或取值为对象/数组时拿到的是同一个引用,就地修改后比较不出变化。
|
|
396
|
+
*/
|
|
397
|
+
let generation = 0;
|
|
398
|
+
let pendingSettlers = [];
|
|
381
399
|
return (asyncValidatorRule, value, callback, source, options) => {
|
|
382
400
|
const actualValue = props.config?.names ? props.model : value;
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
401
|
+
generation += 1;
|
|
402
|
+
const currentGeneration = generation;
|
|
403
|
+
/** 已有更新的一轮校验开始,本轮结论作废 */
|
|
404
|
+
const isStale = () => currentGeneration !== generation;
|
|
405
|
+
pendingSettlers.push((args) => callback(...args));
|
|
406
|
+
/**
|
|
407
|
+
* 结算本轮与被本轮取代的所有在途校验。
|
|
408
|
+
*
|
|
409
|
+
* async-validator 的 callback 不幂等:重复调用会让它的内部计数提前满足、错误信息重复;
|
|
410
|
+
* 原始 validator 写成 async 时很容易既调 callback 又返回 Promise,故所有回调都收敛到这里,
|
|
411
|
+
* 由「取出即出队」保证每个调用的 callback 只会被调用一次。
|
|
412
|
+
*/
|
|
413
|
+
const conclude = (...args) => {
|
|
414
|
+
if (isStale()) return;
|
|
415
|
+
const settlers = pendingSettlers;
|
|
416
|
+
pendingSettlers = [];
|
|
417
|
+
for (const settle of settlers) settle(args);
|
|
418
|
+
};
|
|
419
|
+
/**
|
|
420
|
+
* 执行原始 validator 并把结果统一转成 callback。
|
|
421
|
+
*
|
|
422
|
+
* async-validator 只解析同步返回给它的返回值(true / false / Error / 错误数组 / Promise,
|
|
423
|
+
* 抛错转成错误信息),异步路径已脱离它的调用栈,这些约定会失效导致校验永不结束,故复刻一份。
|
|
424
|
+
*/
|
|
425
|
+
const settleWithOriginalValidator = () => {
|
|
426
|
+
if (isStale()) return;
|
|
427
|
+
if (!originalValidator) {
|
|
428
|
+
conclude();
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
let result;
|
|
432
|
+
try {
|
|
433
|
+
result = originalValidator({
|
|
434
|
+
rule: asyncValidatorRule,
|
|
435
|
+
value: actualValue,
|
|
436
|
+
callback: conclude,
|
|
437
|
+
source,
|
|
438
|
+
options
|
|
439
|
+
}, {
|
|
440
|
+
values: mForm?.initValues || {},
|
|
441
|
+
model: props.model,
|
|
442
|
+
parent: mForm?.parentValues || {},
|
|
443
|
+
formValue: mForm?.values || props.model,
|
|
444
|
+
prop: props.prop,
|
|
445
|
+
config: props.config
|
|
446
|
+
}, mForm);
|
|
447
|
+
} catch (err) {
|
|
448
|
+
conclude(toError(err));
|
|
387
449
|
return;
|
|
388
450
|
}
|
|
389
|
-
|
|
390
|
-
|
|
451
|
+
if (isPromise(result)) result.then(() => conclude(), (err) => conclude(toError(err)));
|
|
452
|
+
else if (result === true) conclude();
|
|
453
|
+
else if (result === false) {
|
|
454
|
+
const field = asyncValidatorRule?.fullField || asyncValidatorRule?.field || props.prop;
|
|
455
|
+
conclude(new Error(rule.message || `${field} fails`));
|
|
456
|
+
} else if (result instanceof Error || Array.isArray(result)) conclude(result);
|
|
457
|
+
};
|
|
458
|
+
const skipFailedTypeMatch = (err) => {
|
|
459
|
+
console.error(err);
|
|
460
|
+
settleWithOriginalValidator();
|
|
461
|
+
};
|
|
462
|
+
let error;
|
|
463
|
+
try {
|
|
464
|
+
error = validateTypeMatch(actualValue, mForm, props, rule.message);
|
|
465
|
+
} catch (err) {
|
|
466
|
+
skipFailedTypeMatch(err);
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
if (isPromise(error)) {
|
|
470
|
+
error.then((asyncMessage) => {
|
|
471
|
+
if (asyncMessage) {
|
|
472
|
+
conclude(new Error(asyncMessage));
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
settleWithOriginalValidator();
|
|
476
|
+
}, skipFailedTypeMatch);
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
if (error) {
|
|
480
|
+
conclude(new Error(error));
|
|
481
|
+
return;
|
|
391
482
|
}
|
|
392
|
-
|
|
393
|
-
rule: asyncValidatorRule,
|
|
394
|
-
value: actualValue,
|
|
395
|
-
callback,
|
|
396
|
-
source,
|
|
397
|
-
options
|
|
398
|
-
}, {
|
|
399
|
-
values: mForm?.initValues || {},
|
|
400
|
-
model: props.model,
|
|
401
|
-
parent: mForm?.parentValues || {},
|
|
402
|
-
formValue: mForm?.values || props.model,
|
|
403
|
-
prop: props.prop,
|
|
404
|
-
config: props.config
|
|
405
|
-
}, mForm);
|
|
406
|
-
callback();
|
|
483
|
+
settleWithOriginalValidator();
|
|
407
484
|
};
|
|
408
485
|
};
|
|
409
486
|
//#endregion
|
package/dist/style.css
CHANGED
|
@@ -296,12 +296,16 @@ fieldset.m-fieldset .m-form-tip {
|
|
|
296
296
|
gap: 8px;
|
|
297
297
|
}
|
|
298
298
|
.m-fields-group-list .m-fields-group-list-footer {
|
|
299
|
-
display: flex;
|
|
300
299
|
justify-content: space-between;
|
|
301
300
|
margin-top: 10px;
|
|
302
301
|
margin-bottom: 16px;
|
|
303
302
|
}
|
|
304
303
|
|
|
304
|
+
/** 最外层的groupList需要每个item需要增加空白区域界限时,增加outer-gorup_list可以实现 */
|
|
305
|
+
.m-container-group-list.outer-gorup_list > .m-fields-group-list > .m-fields-group-list-item {
|
|
306
|
+
margin-bottom: 10px;
|
|
307
|
+
}
|
|
308
|
+
|
|
305
309
|
.m-form-panel .el-card__header:hover {
|
|
306
310
|
background: #f2f6fc;
|
|
307
311
|
}
|
|
@@ -296,12 +296,16 @@ fieldset.m-fieldset .m-form-tip {
|
|
|
296
296
|
gap: 8px;
|
|
297
297
|
}
|
|
298
298
|
.m-fields-group-list .m-fields-group-list-footer {
|
|
299
|
-
display: flex;
|
|
300
299
|
justify-content: space-between;
|
|
301
300
|
margin-top: 10px;
|
|
302
301
|
margin-bottom: 16px;
|
|
303
302
|
}
|
|
304
303
|
|
|
304
|
+
/** 最外层的groupList需要每个item需要增加空白区域界限时,增加outer-gorup_list可以实现 */
|
|
305
|
+
.m-container-group-list.outer-gorup_list > .m-fields-group-list > .m-fields-group-list-item {
|
|
306
|
+
margin-bottom: 10px;
|
|
307
|
+
}
|
|
308
|
+
|
|
305
309
|
.m-form-panel .el-card__header:hover {
|
|
306
310
|
background: #f2f6fc;
|
|
307
311
|
}
|
|
@@ -496,4 +500,10 @@ fieldset.m-fieldset .m-form-tip {
|
|
|
496
500
|
}
|
|
497
501
|
.m-form.m-form--magic-admin .m-form-container .m-form-container-expand {
|
|
498
502
|
text-align: left;
|
|
503
|
+
}
|
|
504
|
+
.m-form.m-form--magic-admin .m-fields-group-list .el-table__empty-block .el-table__empty-text {
|
|
505
|
+
width: 100%;
|
|
506
|
+
background-color: rgba(0, 0, 0, 0.03);
|
|
507
|
+
margin-top: 8px;
|
|
508
|
+
border-radius: 4px;
|
|
499
509
|
}
|