mirror of
https://gitee.com/baidu/amis.git
synced 2024-11-29 18:48:45 +08:00
fix: 修复 combo tabs 模式新成员中有必填字段未填写也能通过校验的问题
This commit is contained in:
parent
3e1e8a6189
commit
c869ea3fbf
@ -50,6 +50,7 @@ import {isAlive} from 'mobx-state-tree';
|
||||
|
||||
import type {LabelAlign} from './Item';
|
||||
import {injectObjectChain} from '../utils';
|
||||
import {reaction} from 'mobx';
|
||||
|
||||
export interface FormHorizontal {
|
||||
left?: number;
|
||||
@ -371,6 +372,7 @@ export interface FormProps
|
||||
onFailed?: (reason: string, errors: any) => any;
|
||||
onFinished: (values: object, action: any) => any;
|
||||
onValidate: (values: object, form: any) => any;
|
||||
onValidChange?: (valid: boolean, props: any) => void; // 表单数据合法性变更
|
||||
messages: {
|
||||
fetchSuccess?: string;
|
||||
fetchFailed?: string;
|
||||
@ -443,6 +445,8 @@ export default class Form extends React.Component<FormProps, object> {
|
||||
'onChange',
|
||||
'onFailed',
|
||||
'onFinished',
|
||||
'onValidate',
|
||||
'onValidChange',
|
||||
'onSaved',
|
||||
'canAccessSuperData',
|
||||
'lazyChange',
|
||||
@ -460,8 +464,7 @@ export default class Form extends React.Component<FormProps, object> {
|
||||
[propName: string]: Array<() => Promise<any>>;
|
||||
} = {};
|
||||
asyncCancel: () => void;
|
||||
disposeOnValidate: () => void;
|
||||
disposeRulesValidate: () => void;
|
||||
toDispose: Array<() => void> = [];
|
||||
shouldLoadInitApi: boolean = false;
|
||||
timer: ReturnType<typeof setTimeout>;
|
||||
mounted: boolean;
|
||||
@ -532,6 +535,7 @@ export default class Form extends React.Component<FormProps, object> {
|
||||
store,
|
||||
messages: {fetchSuccess, fetchFailed},
|
||||
onValidate,
|
||||
onValidChange,
|
||||
promptPageLeave,
|
||||
env,
|
||||
rules
|
||||
@ -541,49 +545,63 @@ export default class Form extends React.Component<FormProps, object> {
|
||||
|
||||
if (onValidate) {
|
||||
const finalValidate = promisify(onValidate);
|
||||
this.disposeOnValidate = this.addHook(async () => {
|
||||
const result = await finalValidate(store.data, store);
|
||||
this.toDispose.push(
|
||||
this.addHook(async () => {
|
||||
const result = await finalValidate(store.data, store);
|
||||
|
||||
if (result && isObject(result)) {
|
||||
Object.keys(result).forEach(key => {
|
||||
let msg = result[key];
|
||||
const items = store.getItemsByPath(key);
|
||||
if (result && isObject(result)) {
|
||||
Object.keys(result).forEach(key => {
|
||||
let msg = result[key];
|
||||
const items = store.getItemsByPath(key);
|
||||
|
||||
// 没有找到
|
||||
if (!Array.isArray(items) || !items.length) {
|
||||
return;
|
||||
}
|
||||
// 没有找到
|
||||
if (!Array.isArray(items) || !items.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 在setError之前,提前把残留的error信息清除掉,否则每次onValidate后都会一直把报错 append 上去
|
||||
items.forEach(item => item.clearError());
|
||||
// 在setError之前,提前把残留的error信息清除掉,否则每次onValidate后都会一直把报错 append 上去
|
||||
items.forEach(item => item.clearError());
|
||||
|
||||
if (msg) {
|
||||
msg = Array.isArray(msg) ? msg : [msg];
|
||||
items.forEach(item => item.addError(msg));
|
||||
}
|
||||
if (msg) {
|
||||
msg = Array.isArray(msg) ? msg : [msg];
|
||||
items.forEach(item => item.addError(msg));
|
||||
}
|
||||
|
||||
delete result[key];
|
||||
});
|
||||
delete result[key];
|
||||
});
|
||||
|
||||
isEmpty(result)
|
||||
? store.clearRestError()
|
||||
: store.setRestError(Object.keys(result).map(key => result[key]));
|
||||
}
|
||||
});
|
||||
isEmpty(result)
|
||||
? store.clearRestError()
|
||||
: store.setRestError(Object.keys(result).map(key => result[key]));
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// 表单校验结果发生变化时,触发 onValidChange
|
||||
if (onValidChange) {
|
||||
this.toDispose.push(
|
||||
reaction(
|
||||
() => store.valid,
|
||||
valid => onValidChange(valid, this.props)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (Array.isArray(rules) && rules.length) {
|
||||
this.disposeRulesValidate = this.addHook(() => {
|
||||
if (!store.valid) {
|
||||
return;
|
||||
}
|
||||
this.toDispose.push(
|
||||
this.addHook(() => {
|
||||
if (!store.valid) {
|
||||
return;
|
||||
}
|
||||
|
||||
rules.forEach(
|
||||
item =>
|
||||
!evalExpression(item.rule, store.data) &&
|
||||
store.addRestError(item.message, item.name)
|
||||
);
|
||||
});
|
||||
rules.forEach(
|
||||
item =>
|
||||
!evalExpression(item.rule, store.data) &&
|
||||
store.addRestError(item.message, item.name)
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (isEffectiveApi(initApi, store.data, initFetch, initFetchOn)) {
|
||||
@ -655,8 +673,8 @@ export default class Form extends React.Component<FormProps, object> {
|
||||
// this.lazyHandleChange.flush();
|
||||
this.lazyEmitChange.cancel();
|
||||
this.asyncCancel && this.asyncCancel();
|
||||
this.disposeOnValidate && this.disposeOnValidate();
|
||||
this.disposeRulesValidate && this.disposeRulesValidate();
|
||||
this.toDispose.forEach(fn => fn());
|
||||
this.toDispose = [];
|
||||
window.removeEventListener('beforeunload', this.beforePageUnload);
|
||||
this.unBlockRouting?.();
|
||||
}
|
||||
@ -836,30 +854,27 @@ export default class Form extends React.Component<FormProps, object> {
|
||||
return this.props.store.validated;
|
||||
}
|
||||
|
||||
validate(
|
||||
async validate(
|
||||
forceValidate?: boolean,
|
||||
throwErrors: boolean = false
|
||||
throwErrors: boolean = false,
|
||||
toastErrors: boolean = true
|
||||
): Promise<boolean> {
|
||||
const {store, dispatchEvent, data, messages, translate: __} = this.props;
|
||||
|
||||
this.flush();
|
||||
return store
|
||||
.validate(
|
||||
this.hooks['validate'] || [],
|
||||
forceValidate,
|
||||
throwErrors,
|
||||
typeof messages?.validateFailed === 'string'
|
||||
? __(filter(messages.validateFailed, store.data))
|
||||
: undefined
|
||||
)
|
||||
.then((result: boolean) => {
|
||||
if (result) {
|
||||
dispatchEvent('validateSucc', data);
|
||||
} else {
|
||||
dispatchEvent('validateError', data);
|
||||
}
|
||||
return result;
|
||||
});
|
||||
const result = await store.validate(
|
||||
this.hooks['validate'] || [],
|
||||
forceValidate,
|
||||
throwErrors,
|
||||
toastErrors === false
|
||||
? ''
|
||||
: typeof messages?.validateFailed === 'string'
|
||||
? __(filter(messages.validateFailed, store.data))
|
||||
: undefined
|
||||
);
|
||||
|
||||
dispatchEvent(result ? 'validateSucc' : 'validateError', data);
|
||||
return result;
|
||||
}
|
||||
|
||||
setErrors(errors: {[propName: string]: string}, tag = 'remote') {
|
||||
|
@ -35,7 +35,8 @@ export const ComboStore = iRendererStore
|
||||
minLength: 0,
|
||||
maxLength: 0,
|
||||
length: 0,
|
||||
activeKey: 0
|
||||
activeKey: 0,
|
||||
memberValidMap: types.optional(types.frozen(), {})
|
||||
})
|
||||
.views(self => {
|
||||
function getForms() {
|
||||
@ -170,13 +171,21 @@ export const ComboStore = iRendererStore
|
||||
self.activeKey = key;
|
||||
}
|
||||
|
||||
function setMemberValid(valid: boolean, index: number) {
|
||||
self.memberValidMap = {
|
||||
...self.memberValidMap,
|
||||
[index]: valid
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
config,
|
||||
setActiveKey,
|
||||
bindUniuqueItem,
|
||||
unBindUniuqueItem,
|
||||
addForm,
|
||||
onChildStoreDispose
|
||||
onChildStoreDispose,
|
||||
setMemberValid
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -2053,6 +2053,7 @@
|
||||
--Tabs-onActive-bg: var(--background);
|
||||
--Tabs-onActive-borderColor: var(--borderColor);
|
||||
--Tabs-onActive-color: var(--colors-neutral-text-2);
|
||||
--Tabs-onError-color: var(--colors-error-5);
|
||||
--Tabs-onDisabled-color: var(--colors-neutral-text-7);
|
||||
--Tabs-onHover-borderColor: var(--colors-neutral-line-8);
|
||||
--Tabs-add-icon-size: #{px2rem(15px)};
|
||||
@ -4131,6 +4132,7 @@
|
||||
var(--combo-vertical-right-border-color)
|
||||
var(--combo-vertical-bottom-border-color)
|
||||
var(--combo-vertical-left-border-color);
|
||||
--Combo--vertical-item--onError-borderColor: var(--colors-error-5);
|
||||
--Combo--vertical-item-borderRadius: var(
|
||||
--combo-vertical-top-left-border-radius
|
||||
)
|
||||
|
@ -242,6 +242,10 @@
|
||||
border-color: var(--Tabs-onActive-borderColor);
|
||||
border-bottom-color: transparent;
|
||||
}
|
||||
|
||||
&.has-error > a:first-child {
|
||||
color: var(--Tabs-onError-color) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -258,6 +258,12 @@
|
||||
var(--combo-vertical-paddingRight) var(--combo-vertical-paddingBottom)
|
||||
var(--combo-vertical-paddingLeft);
|
||||
position: relative;
|
||||
|
||||
&.has-error {
|
||||
border-color: var(
|
||||
--Combo--vertical-item--onError-borderColor
|
||||
) !important; // 因为下面的规则权重更高 &:not(.is-disabled) > .#{$ns}Combo-items > .#{$ns}Combo-item:hover
|
||||
}
|
||||
}
|
||||
|
||||
> .#{$ns}Combo-items > .#{$ns}Combo-item {
|
||||
|
@ -50,6 +50,7 @@ export interface TabProps extends ThemeProps {
|
||||
tip?: string;
|
||||
tab?: Schema;
|
||||
className?: string;
|
||||
tabClassName?: string;
|
||||
activeKey?: string | number;
|
||||
reload?: boolean;
|
||||
mountOnEnter?: boolean;
|
||||
|
@ -8,7 +8,10 @@ import {
|
||||
resolveEventData,
|
||||
ApiObject,
|
||||
FormHorizontal,
|
||||
evalExpressionWithConditionBuilder
|
||||
evalExpressionWithConditionBuilder,
|
||||
IFormStore,
|
||||
getVariable,
|
||||
IFormItemStore
|
||||
} from 'amis-core';
|
||||
import {ActionObject, Api} from 'amis-core';
|
||||
import {ComboStore, IComboStore} from 'amis-core';
|
||||
@ -37,7 +40,11 @@ import {isEffectiveApi, str2AsyncFunction} from 'amis-core';
|
||||
import {Alert2} from 'amis-ui';
|
||||
import memoize from 'lodash/memoize';
|
||||
import {Icon} from 'amis-ui';
|
||||
import {isAlive} from 'mobx-state-tree';
|
||||
import {
|
||||
isAlive,
|
||||
clone as cloneModel,
|
||||
destroy as destroyModel
|
||||
} from 'mobx-state-tree';
|
||||
import {
|
||||
FormBaseControlSchema,
|
||||
SchemaApi,
|
||||
@ -48,7 +55,6 @@ import {
|
||||
import {ListenerAction} from 'amis-core';
|
||||
import type {SchemaTokenizeableString} from '../../Schema';
|
||||
import isPlainObject from 'lodash/isPlainObject';
|
||||
import {isMobile} from 'amis-core';
|
||||
|
||||
export type ComboCondition = {
|
||||
test: string;
|
||||
@ -395,6 +401,7 @@ export default class ComboControl extends React.Component<ComboProps> {
|
||||
this.dragTipRef = this.dragTipRef.bind(this);
|
||||
this.flush = this.flush.bind(this);
|
||||
this.handleComboTypeChange = this.handleComboTypeChange.bind(this);
|
||||
this.handleSubFormValid = this.handleSubFormValid.bind(this);
|
||||
this.defaultValue = {
|
||||
...props.scaffold
|
||||
};
|
||||
@ -797,6 +804,11 @@ export default class ComboControl extends React.Component<ComboProps> {
|
||||
);
|
||||
}
|
||||
|
||||
handleSubFormValid(valid: boolean, {index}: any) {
|
||||
const {store} = this.props;
|
||||
store.setMemberValid(valid, index);
|
||||
}
|
||||
|
||||
handleFormInit(values: any, {index}: any) {
|
||||
const {
|
||||
syncDefaultValue,
|
||||
@ -806,9 +818,15 @@ export default class ComboControl extends React.Component<ComboProps> {
|
||||
formInited,
|
||||
onChange,
|
||||
submitOnChange,
|
||||
setPrinstineValue
|
||||
setPrinstineValue,
|
||||
formItem
|
||||
} = this.props;
|
||||
|
||||
// 已经开始验证了,那么打开成员的时候,就要验证一下。
|
||||
if (formItem?.validated) {
|
||||
this.subForms[index]?.validate(true, false, false);
|
||||
}
|
||||
|
||||
this.subFormDefaultValues.push({
|
||||
index,
|
||||
values,
|
||||
@ -881,7 +899,13 @@ export default class ComboControl extends React.Component<ComboProps> {
|
||||
}
|
||||
|
||||
validate(): any {
|
||||
const {messages, nullable, translate: __} = this.props;
|
||||
const {
|
||||
messages,
|
||||
nullable,
|
||||
value: rawValue,
|
||||
translate: __,
|
||||
store
|
||||
} = this.props;
|
||||
const value = this.getValueAsArray();
|
||||
const minLength = this.resolveVariableProps(this.props, 'minLength');
|
||||
const maxLength = this.resolveVariableProps(this.props, 'maxLength');
|
||||
@ -896,18 +920,62 @@ export default class ComboControl extends React.Component<ComboProps> {
|
||||
(messages && messages.maxLengthValidateFailed) || 'Combo.maxLength',
|
||||
{maxLength}
|
||||
);
|
||||
} else if (this.subForms.length && (!nullable || value)) {
|
||||
return Promise.all(this.subForms.map(item => item.validate())).then(
|
||||
values => {
|
||||
if (~values.indexOf(false)) {
|
||||
return __(
|
||||
(messages && messages.validateFailed) || 'validateFailed'
|
||||
);
|
||||
}
|
||||
} else if (nullable && !rawValue) {
|
||||
return; // 不校验
|
||||
} else if (value.length) {
|
||||
return Promise.all(
|
||||
value.map(async (values: any, index: number) => {
|
||||
const subForm = this.subForms[index];
|
||||
if (subForm) {
|
||||
return subForm.validate(true, false, false);
|
||||
} else {
|
||||
// 那些还没有渲染出来的数据
|
||||
// 因为有可能存在分页,有可能存在懒加载,所以没办法直接用 subForm 去校验了
|
||||
const subForm = this.subForms[Object.keys(this.subForms)[0] as any];
|
||||
if (subForm) {
|
||||
const form: IFormStore = subForm.props.store;
|
||||
let valid = false;
|
||||
for (let formitem of form.items) {
|
||||
const cloned: IFormItemStore = cloneModel(formitem);
|
||||
let value: any = getVariable(values, formitem.name, false);
|
||||
|
||||
return;
|
||||
if (formitem.extraName) {
|
||||
value = [
|
||||
getVariable(values, formitem.name, false),
|
||||
getVariable(values, formitem.extraName, false)
|
||||
];
|
||||
}
|
||||
|
||||
cloned.changeTmpValue(value, 'dataChanged');
|
||||
valid = await cloned.validate(values);
|
||||
destroyModel(cloned);
|
||||
if (valid === false) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
store.setMemberValid(valid, index);
|
||||
return valid;
|
||||
}
|
||||
}
|
||||
})
|
||||
).then(values => {
|
||||
if (~values.indexOf(false)) {
|
||||
return __((messages && messages.validateFailed) || 'validateFailed');
|
||||
}
|
||||
);
|
||||
|
||||
return;
|
||||
});
|
||||
} else if (this.subForms.length) {
|
||||
return Promise.all(
|
||||
this.subForms.map(item => item.validate(true, false, false))
|
||||
).then(values => {
|
||||
if (~values.indexOf(false)) {
|
||||
return __((messages && messages.validateFailed) || 'validateFailed');
|
||||
}
|
||||
|
||||
return;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -1253,6 +1321,12 @@ export default class ComboControl extends React.Component<ComboProps> {
|
||||
// 不能按需渲染,因为 unique 会失效。
|
||||
mountOnEnter={!hasUnique}
|
||||
unmountOnExit={false}
|
||||
className={
|
||||
store.memberValidMap[index] === false ? 'has-error' : ''
|
||||
}
|
||||
tabClassName={
|
||||
store.memberValidMap[index] === false ? 'has-error' : ''
|
||||
}
|
||||
>
|
||||
{condition && typeSwitchable !== false ? (
|
||||
<div className={cx('Combo-itemTag')}>
|
||||
@ -1485,7 +1559,8 @@ export default class ComboControl extends React.Component<ComboProps> {
|
||||
itemClassName,
|
||||
itemsWrapperClassName,
|
||||
static: isStatic,
|
||||
mobileUI
|
||||
mobileUI,
|
||||
store
|
||||
} = this.props;
|
||||
|
||||
let items = this.props.items;
|
||||
@ -1543,7 +1618,11 @@ export default class ComboControl extends React.Component<ComboProps> {
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx(`Combo-item`, itemClassName)}
|
||||
className={cx(
|
||||
`Combo-item`,
|
||||
itemClassName,
|
||||
store.memberValidMap[index] === false ? 'has-error' : ''
|
||||
)}
|
||||
key={this.keys[index]}
|
||||
>
|
||||
{!isStatic && !disabled && draggable && thelist.length > 1 ? (
|
||||
@ -1622,7 +1701,8 @@ export default class ComboControl extends React.Component<ComboProps> {
|
||||
nullable,
|
||||
translate: __,
|
||||
itemClassName,
|
||||
mobileUI
|
||||
mobileUI,
|
||||
store
|
||||
} = this.props;
|
||||
|
||||
let items = this.props.items;
|
||||
@ -1646,7 +1726,13 @@ export default class ComboControl extends React.Component<ComboProps> {
|
||||
disabled ? 'is-disabled' : ''
|
||||
)}
|
||||
>
|
||||
<div className={cx(`Combo-item`, itemClassName)}>
|
||||
<div
|
||||
className={cx(
|
||||
`Combo-item`,
|
||||
itemClassName,
|
||||
store.memberValidMap[0] === false ? 'has-error' : ''
|
||||
)}
|
||||
>
|
||||
{condition && typeSwitchable !== false ? (
|
||||
<div className={cx('Combo-itemTag')}>
|
||||
<label>{__('Combo.type')}</label>
|
||||
@ -1715,11 +1801,13 @@ export default class ComboControl extends React.Component<ComboProps> {
|
||||
className: cx(`Combo-form`, formClassName)
|
||||
},
|
||||
{
|
||||
index: 0,
|
||||
disabled: disabled,
|
||||
static: isStatic,
|
||||
data,
|
||||
onChange: this.handleSingleFormChange,
|
||||
ref: this.makeFormRef(0),
|
||||
onValidChange: this.handleSubFormValid,
|
||||
onInit: this.handleSingleFormInit,
|
||||
canAccessSuperData,
|
||||
formStore: undefined,
|
||||
@ -1749,6 +1837,7 @@ export default class ComboControl extends React.Component<ComboProps> {
|
||||
onAction: this.handleAction,
|
||||
onRadioChange: this.handleRadioChange,
|
||||
ref: this.makeFormRef(index),
|
||||
onValidChange: this.handleSubFormValid,
|
||||
canAccessSuperData,
|
||||
lazyChange: changeImmediately ? false : true,
|
||||
formLazyChange: false,
|
||||
|
Loading…
Reference in New Issue
Block a user