680 lines
22 KiB
Vue
680 lines
22 KiB
Vue
import AsyncValidator from 'async-validator';
|
|
import warning from 'warning';
|
|
import get from 'lodash/get';
|
|
import set from 'lodash/set';
|
|
import omit from 'lodash/omit';
|
|
import createFieldsStore from './createFieldsStore';
|
|
import { cloneElement } from '../../_util/vnode';
|
|
import BaseMixin from '../../_util/BaseMixin';
|
|
import { getOptionProps, getEvents } from '../../_util/props-util';
|
|
import PropTypes from '../../_util/vue-types';
|
|
|
|
import {
|
|
argumentContainer,
|
|
identity,
|
|
normalizeValidateRules,
|
|
getValidateTriggers,
|
|
getValueFromEvent,
|
|
hasRules,
|
|
getParams,
|
|
isEmptyObject,
|
|
flattenArray,
|
|
} from './utils';
|
|
|
|
const DEFAULT_TRIGGER = 'change';
|
|
|
|
function createBaseForm(option = {}, mixins = []) {
|
|
const {
|
|
validateMessages,
|
|
onFieldsChange,
|
|
onValuesChange,
|
|
mapProps = identity,
|
|
mapPropsToFields,
|
|
fieldNameProp,
|
|
fieldMetaProp,
|
|
fieldDataProp,
|
|
formPropName = 'form',
|
|
name: formName,
|
|
props = {},
|
|
templateContext,
|
|
} = option;
|
|
return function decorate(WrappedComponent) {
|
|
let formProps = {};
|
|
if (Array.isArray(props)) {
|
|
props.forEach(prop => {
|
|
formProps[prop] = PropTypes.any;
|
|
});
|
|
} else {
|
|
formProps = props;
|
|
}
|
|
const Form = {
|
|
mixins: [BaseMixin, ...mixins],
|
|
props: {
|
|
...formProps,
|
|
wrappedComponentRef: PropTypes.func.def(() => {}),
|
|
},
|
|
data() {
|
|
const fields = mapPropsToFields && mapPropsToFields(this.$props);
|
|
this.fieldsStore = createFieldsStore(fields || {});
|
|
this.templateContext = templateContext;
|
|
this.instances = {};
|
|
this.cachedBind = {};
|
|
this.clearedFieldMetaCache = {};
|
|
|
|
this.renderFields = {};
|
|
this.domFields = {};
|
|
|
|
// HACK: https://github.com/ant-design/ant-design/issues/6406
|
|
[
|
|
'getFieldsValue',
|
|
'getFieldValue',
|
|
'setFieldsInitialValue',
|
|
'getFieldsError',
|
|
'getFieldError',
|
|
'isFieldValidating',
|
|
'isFieldsValidating',
|
|
'isFieldsTouched',
|
|
'isFieldTouched',
|
|
].forEach(key => {
|
|
this[key] = (...args) => {
|
|
return this.fieldsStore[key](...args);
|
|
};
|
|
});
|
|
|
|
return {
|
|
submitting: false,
|
|
};
|
|
},
|
|
watch: templateContext
|
|
? {}
|
|
: {
|
|
$props: {
|
|
handler: function(nextProps) {
|
|
if (mapPropsToFields) {
|
|
this.fieldsStore.updateFields(mapPropsToFields(nextProps));
|
|
}
|
|
},
|
|
deep: true,
|
|
},
|
|
},
|
|
mounted() {
|
|
this.cleanUpUselessFields();
|
|
},
|
|
updated() {
|
|
// form updated add for template v-decorator
|
|
this.cleanUpUselessFields();
|
|
},
|
|
methods: {
|
|
updateFields(fields = {}) {
|
|
this.fieldsStore.updateFields(mapPropsToFields(fields));
|
|
if (templateContext) {
|
|
templateContext.$forceUpdate();
|
|
}
|
|
},
|
|
onCollectCommon(name, action, args) {
|
|
const fieldMeta = this.fieldsStore.getFieldMeta(name);
|
|
if (fieldMeta[action]) {
|
|
fieldMeta[action](...args);
|
|
} else if (fieldMeta.originalProps && fieldMeta.originalProps[action]) {
|
|
fieldMeta.originalProps[action](...args);
|
|
}
|
|
const value = fieldMeta.getValueFromEvent
|
|
? fieldMeta.getValueFromEvent(...args)
|
|
: getValueFromEvent(...args);
|
|
if (onValuesChange && value !== this.fieldsStore.getFieldValue(name)) {
|
|
const valuesAll = this.fieldsStore.getAllValues();
|
|
const valuesAllSet = {};
|
|
valuesAll[name] = value;
|
|
Object.keys(valuesAll).forEach(key => set(valuesAllSet, key, valuesAll[key]));
|
|
onValuesChange(this, set({}, name, value), valuesAllSet);
|
|
}
|
|
const field = this.fieldsStore.getField(name);
|
|
return { name, field: { ...field, value, touched: true }, fieldMeta };
|
|
},
|
|
|
|
onCollect(name_, action, ...args) {
|
|
const { name, field, fieldMeta } = this.onCollectCommon(name_, action, args);
|
|
const { validate } = fieldMeta;
|
|
const newField = {
|
|
...field,
|
|
dirty: hasRules(validate),
|
|
};
|
|
this.setFields({
|
|
[name]: newField,
|
|
});
|
|
},
|
|
|
|
onCollectValidate(name_, action, ...args) {
|
|
const { field, fieldMeta } = this.onCollectCommon(name_, action, args);
|
|
const newField = {
|
|
...field,
|
|
dirty: true,
|
|
};
|
|
this.validateFieldsInternal([newField], {
|
|
action,
|
|
options: {
|
|
firstFields: !!fieldMeta.validateFirst,
|
|
},
|
|
});
|
|
},
|
|
|
|
getCacheBind(name, action, fn) {
|
|
if (!this.cachedBind[name]) {
|
|
this.cachedBind[name] = {};
|
|
}
|
|
const cache = this.cachedBind[name];
|
|
if (!cache[action] || cache[action].oriFn !== fn) {
|
|
cache[action] = {
|
|
fn: fn.bind(this, name, action),
|
|
oriFn: fn,
|
|
};
|
|
}
|
|
return cache[action].fn;
|
|
},
|
|
|
|
getFieldDecorator(name, fieldOption) {
|
|
const { props, ...restProps } = this.getFieldProps(name, fieldOption);
|
|
return fieldElem => {
|
|
// We should put field in record if it is rendered
|
|
this.renderFields[name] = true;
|
|
|
|
const fieldMeta = this.fieldsStore.getFieldMeta(name);
|
|
const originalProps = getOptionProps(fieldElem);
|
|
const originalEvents = getEvents(fieldElem);
|
|
if (process.env.NODE_ENV !== 'production') {
|
|
const valuePropName = fieldMeta.valuePropName;
|
|
warning(
|
|
!(valuePropName in originalProps),
|
|
`\`getFieldDecorator\` will override \`${valuePropName}\`, ` +
|
|
`so please don't set \`${valuePropName} and v-model\` directly ` +
|
|
`and use \`setFieldsValue\` to set it.`,
|
|
);
|
|
const defaultValuePropName = `default${valuePropName[0].toUpperCase()}${valuePropName.slice(
|
|
1,
|
|
)}`;
|
|
warning(
|
|
!(defaultValuePropName in originalProps),
|
|
`\`${defaultValuePropName}\` is invalid ` +
|
|
`for \`getFieldDecorator\` will set \`${valuePropName}\`,` +
|
|
` please use \`option.initialValue\` instead.`,
|
|
);
|
|
}
|
|
fieldMeta.originalProps = originalProps;
|
|
// fieldMeta.ref = fieldElem.data && fieldElem.data.ref
|
|
const newProps = {
|
|
props: {
|
|
...props,
|
|
...this.fieldsStore.getFieldValuePropValue(fieldMeta),
|
|
},
|
|
...restProps,
|
|
};
|
|
newProps.domProps.value = newProps.props.value;
|
|
const newEvents = {};
|
|
Object.keys(newProps.on).forEach(key => {
|
|
if (originalEvents[key]) {
|
|
const triggerEvents = newProps.on[key];
|
|
newEvents[key] = (...args) => {
|
|
originalEvents[key](...args);
|
|
triggerEvents(...args);
|
|
};
|
|
} else {
|
|
newEvents[key] = newProps.on[key];
|
|
}
|
|
});
|
|
return cloneElement(fieldElem, { ...newProps, on: newEvents });
|
|
};
|
|
},
|
|
|
|
getFieldProps(name, usersFieldOption = {}) {
|
|
if (!name) {
|
|
throw new Error('Must call `getFieldProps` with valid name string!');
|
|
}
|
|
if (process.env.NODE_ENV !== 'production') {
|
|
warning(
|
|
this.fieldsStore.isValidNestedFieldName(name),
|
|
'One field name cannot be part of another, e.g. `a` and `a.b`.',
|
|
);
|
|
warning(
|
|
!('exclusive' in usersFieldOption),
|
|
'`option.exclusive` of `getFieldProps`|`getFieldDecorator` had been remove.',
|
|
);
|
|
}
|
|
|
|
delete this.clearedFieldMetaCache[name];
|
|
|
|
const fieldOption = {
|
|
name,
|
|
trigger: DEFAULT_TRIGGER,
|
|
valuePropName: 'value',
|
|
validate: [],
|
|
...usersFieldOption,
|
|
};
|
|
|
|
const { rules, trigger, validateTrigger = trigger, validate } = fieldOption;
|
|
|
|
const fieldMeta = this.fieldsStore.getFieldMeta(name);
|
|
if ('initialValue' in fieldOption) {
|
|
fieldMeta.initialValue = fieldOption.initialValue;
|
|
}
|
|
|
|
const inputProps = {
|
|
...this.fieldsStore.getFieldValuePropValue(fieldOption),
|
|
// ref: name,
|
|
};
|
|
const inputListeners = {};
|
|
const inputAttrs = {};
|
|
if (fieldNameProp) {
|
|
inputProps[fieldNameProp] = formName ? `${formName}_${name}` : name;
|
|
}
|
|
|
|
const validateRules = normalizeValidateRules(validate, rules, validateTrigger);
|
|
const validateTriggers = getValidateTriggers(validateRules);
|
|
validateTriggers.forEach(action => {
|
|
if (inputListeners[action]) return;
|
|
inputListeners[action] = this.getCacheBind(name, action, this.onCollectValidate);
|
|
});
|
|
|
|
// make sure that the value will be collect
|
|
if (trigger && validateTriggers.indexOf(trigger) === -1) {
|
|
inputListeners[trigger] = this.getCacheBind(name, trigger, this.onCollect);
|
|
}
|
|
|
|
const meta = {
|
|
...fieldMeta,
|
|
...fieldOption,
|
|
validate: validateRules,
|
|
};
|
|
this.fieldsStore.setFieldMeta(name, meta);
|
|
if (fieldMetaProp) {
|
|
inputAttrs[fieldMetaProp] = meta;
|
|
}
|
|
|
|
if (fieldDataProp) {
|
|
inputAttrs[fieldDataProp] = this.fieldsStore.getField(name);
|
|
}
|
|
// This field is rendered, record it
|
|
this.renderFields[name] = true;
|
|
return {
|
|
props: omit(inputProps, ['id']),
|
|
// id: inputProps.id,
|
|
domProps: {
|
|
value: inputProps.value,
|
|
},
|
|
attrs: {
|
|
...inputAttrs,
|
|
id: inputProps.id,
|
|
},
|
|
directives: [
|
|
{
|
|
name: 'ant-ref',
|
|
value: this.getCacheBind(name, `${name}__ref`, this.saveRef),
|
|
},
|
|
],
|
|
on: inputListeners,
|
|
};
|
|
},
|
|
|
|
getFieldInstance(name) {
|
|
return this.instances[name];
|
|
},
|
|
|
|
getRules(fieldMeta, action) {
|
|
const actionRules = fieldMeta.validate
|
|
.filter(item => {
|
|
return !action || item.trigger.indexOf(action) >= 0;
|
|
})
|
|
.map(item => item.rules);
|
|
return flattenArray(actionRules);
|
|
},
|
|
|
|
setFields(maybeNestedFields, callback) {
|
|
const fields = this.fieldsStore.flattenRegisteredFields(maybeNestedFields);
|
|
this.fieldsStore.setFields(fields);
|
|
if (onFieldsChange) {
|
|
const changedFields = Object.keys(fields).reduce(
|
|
(acc, name) => set(acc, name, this.fieldsStore.getField(name)),
|
|
{},
|
|
);
|
|
onFieldsChange(this, changedFields, this.fieldsStore.getNestedAllFields());
|
|
}
|
|
if (templateContext) {
|
|
templateContext.$forceUpdate();
|
|
} else {
|
|
this.$forceUpdate();
|
|
}
|
|
this.$nextTick(() => {
|
|
callback && callback();
|
|
});
|
|
},
|
|
|
|
setFieldsValue(changedValues, callback) {
|
|
const { fieldsMeta } = this.fieldsStore;
|
|
const values = this.fieldsStore.flattenRegisteredFields(changedValues);
|
|
const newFields = Object.keys(values).reduce((acc, name) => {
|
|
const isRegistered = fieldsMeta[name];
|
|
if (process.env.NODE_ENV !== 'production') {
|
|
warning(
|
|
isRegistered,
|
|
'Cannot use `setFieldsValue` until ' +
|
|
'you use `getFieldDecorator` or `getFieldProps` to register it.',
|
|
);
|
|
}
|
|
if (isRegistered) {
|
|
const value = values[name];
|
|
acc[name] = {
|
|
value,
|
|
};
|
|
}
|
|
return acc;
|
|
}, {});
|
|
this.setFields(newFields, callback);
|
|
if (onValuesChange) {
|
|
const allValues = this.fieldsStore.getAllValues();
|
|
onValuesChange(this, changedValues, allValues);
|
|
}
|
|
},
|
|
|
|
saveRef(name, _, component) {
|
|
if (!component) {
|
|
const fieldMeta = this.fieldsStore.getFieldMeta(name);
|
|
if (!fieldMeta.preserve) {
|
|
// after destroy, delete data
|
|
this.clearedFieldMetaCache[name] = {
|
|
field: this.fieldsStore.getField(name),
|
|
meta: fieldMeta,
|
|
};
|
|
this.clearField(name);
|
|
}
|
|
delete this.domFields[name];
|
|
return;
|
|
}
|
|
this.domFields[name] = true;
|
|
this.recoverClearedField(name);
|
|
// const fieldMeta = this.fieldsStore.getFieldMeta(name)
|
|
// if (fieldMeta) {
|
|
// const ref = fieldMeta.ref
|
|
// if (ref) {
|
|
// if (typeof ref === 'string') {
|
|
// throw new Error(`can not set ref string for ${name}`)
|
|
// }
|
|
// ref(component)
|
|
// }
|
|
// }
|
|
this.instances[name] = component;
|
|
},
|
|
|
|
cleanUpUselessFields() {
|
|
const fieldList = this.fieldsStore.getAllFieldsName();
|
|
const removedList = fieldList.filter(field => {
|
|
const fieldMeta = this.fieldsStore.getFieldMeta(field);
|
|
return !this.renderFields[field] && !this.domFields[field] && !fieldMeta.preserve;
|
|
});
|
|
if (removedList.length) {
|
|
removedList.forEach(this.clearField);
|
|
}
|
|
this.renderFields = {};
|
|
},
|
|
|
|
clearField(name) {
|
|
this.fieldsStore.clearField(name);
|
|
delete this.instances[name];
|
|
delete this.cachedBind[name];
|
|
},
|
|
|
|
resetFields(ns) {
|
|
const newFields = this.fieldsStore.resetFields(ns);
|
|
if (Object.keys(newFields).length > 0) {
|
|
this.setFields(newFields);
|
|
}
|
|
if (ns) {
|
|
const names = Array.isArray(ns) ? ns : [ns];
|
|
names.forEach(name => delete this.clearedFieldMetaCache[name]);
|
|
} else {
|
|
this.clearedFieldMetaCache = {};
|
|
}
|
|
},
|
|
|
|
recoverClearedField(name) {
|
|
if (this.clearedFieldMetaCache[name]) {
|
|
this.fieldsStore.setFields({
|
|
[name]: this.clearedFieldMetaCache[name].field,
|
|
});
|
|
this.fieldsStore.setFieldMeta(name, this.clearedFieldMetaCache[name].meta);
|
|
delete this.clearedFieldMetaCache[name];
|
|
}
|
|
},
|
|
|
|
validateFieldsInternal(fields, { fieldNames, action, options = {} }, callback) {
|
|
const allRules = {};
|
|
const allValues = {};
|
|
const allFields = {};
|
|
const alreadyErrors = {};
|
|
fields.forEach(field => {
|
|
const name = field.name;
|
|
if (options.force !== true && field.dirty === false) {
|
|
if (field.errors) {
|
|
set(alreadyErrors, name, { errors: field.errors });
|
|
}
|
|
return;
|
|
}
|
|
const fieldMeta = this.fieldsStore.getFieldMeta(name);
|
|
const newField = {
|
|
...field,
|
|
};
|
|
newField.errors = undefined;
|
|
newField.validating = true;
|
|
newField.dirty = true;
|
|
allRules[name] = this.getRules(fieldMeta, action);
|
|
allValues[name] = newField.value;
|
|
allFields[name] = newField;
|
|
});
|
|
this.setFields(allFields);
|
|
// in case normalize
|
|
Object.keys(allValues).forEach(f => {
|
|
allValues[f] = this.fieldsStore.getFieldValue(f);
|
|
});
|
|
if (callback && isEmptyObject(allFields)) {
|
|
callback(
|
|
isEmptyObject(alreadyErrors) ? null : alreadyErrors,
|
|
this.fieldsStore.getFieldsValue(fieldNames),
|
|
);
|
|
return;
|
|
}
|
|
const validator = new AsyncValidator(allRules);
|
|
if (validateMessages) {
|
|
validator.messages(validateMessages);
|
|
}
|
|
validator.validate(allValues, options, errors => {
|
|
const errorsGroup = {
|
|
...alreadyErrors,
|
|
};
|
|
if (errors && errors.length) {
|
|
errors.forEach(e => {
|
|
const fieldName = e.field;
|
|
const field = get(errorsGroup, fieldName);
|
|
if (typeof field !== 'object' || Array.isArray(field)) {
|
|
set(errorsGroup, fieldName, { errors: [] });
|
|
}
|
|
const fieldErrors = get(errorsGroup, fieldName.concat('.errors'));
|
|
fieldErrors.push(e);
|
|
});
|
|
}
|
|
const expired = [];
|
|
const nowAllFields = {};
|
|
Object.keys(allRules).forEach(name => {
|
|
const fieldErrors = get(errorsGroup, name);
|
|
const nowField = this.fieldsStore.getField(name);
|
|
// avoid concurrency problems
|
|
if (nowField.value !== allValues[name]) {
|
|
expired.push({
|
|
name,
|
|
});
|
|
} else {
|
|
nowField.errors = fieldErrors && fieldErrors.errors;
|
|
nowField.value = allValues[name];
|
|
nowField.validating = false;
|
|
nowField.dirty = false;
|
|
nowAllFields[name] = nowField;
|
|
}
|
|
});
|
|
this.setFields(nowAllFields);
|
|
if (callback) {
|
|
if (expired.length) {
|
|
expired.forEach(({ name }) => {
|
|
const fieldErrors = [
|
|
{
|
|
message: `${name} need to revalidate`,
|
|
field: name,
|
|
},
|
|
];
|
|
set(errorsGroup, name, {
|
|
expired: true,
|
|
errors: fieldErrors,
|
|
});
|
|
});
|
|
}
|
|
|
|
callback(
|
|
isEmptyObject(errorsGroup) ? null : errorsGroup,
|
|
this.fieldsStore.getFieldsValue(fieldNames),
|
|
);
|
|
}
|
|
});
|
|
},
|
|
|
|
validateFields(ns, opt, cb) {
|
|
const pending = new Promise((resolve, reject) => {
|
|
const { names, options } = getParams(ns, opt, cb);
|
|
let { callback } = getParams(ns, opt, cb);
|
|
if (!callback || typeof callback === 'function') {
|
|
const oldCb = callback;
|
|
callback = (errors, values) => {
|
|
if (oldCb) {
|
|
oldCb(errors, values);
|
|
} else if (errors) {
|
|
reject({ errors, values });
|
|
} else {
|
|
resolve(values);
|
|
}
|
|
};
|
|
}
|
|
const fieldNames = names
|
|
? this.fieldsStore.getValidFieldsFullName(names)
|
|
: this.fieldsStore.getValidFieldsName();
|
|
const fields = fieldNames
|
|
.filter(name => {
|
|
const fieldMeta = this.fieldsStore.getFieldMeta(name);
|
|
return hasRules(fieldMeta.validate);
|
|
})
|
|
.map(name => {
|
|
const field = this.fieldsStore.getField(name);
|
|
field.value = this.fieldsStore.getFieldValue(name);
|
|
return field;
|
|
});
|
|
if (!fields.length) {
|
|
if (callback) {
|
|
callback(null, this.fieldsStore.getFieldsValue(fieldNames));
|
|
}
|
|
return;
|
|
}
|
|
if (!('firstFields' in options)) {
|
|
options.firstFields = fieldNames.filter(name => {
|
|
const fieldMeta = this.fieldsStore.getFieldMeta(name);
|
|
return !!fieldMeta.validateFirst;
|
|
});
|
|
}
|
|
this.validateFieldsInternal(
|
|
fields,
|
|
{
|
|
fieldNames,
|
|
options,
|
|
},
|
|
callback,
|
|
);
|
|
});
|
|
pending.catch(e => {
|
|
if (console.error) {
|
|
console.error(e);
|
|
}
|
|
return e;
|
|
});
|
|
return pending;
|
|
},
|
|
|
|
isSubmitting() {
|
|
if (process.env.NODE_ENV !== 'production' && process.env.NODE_ENV !== 'test') {
|
|
warning(
|
|
false,
|
|
'`isSubmitting` is deprecated. ' +
|
|
"Actually, it's more convenient to handle submitting status by yourself.",
|
|
);
|
|
}
|
|
return this.submitting;
|
|
},
|
|
|
|
submit(callback) {
|
|
if (process.env.NODE_ENV !== 'production' && process.env.NODE_ENV !== 'test') {
|
|
warning(
|
|
false,
|
|
'`submit` is deprecated.' +
|
|
"Actually, it's more convenient to handle submitting status by yourself.",
|
|
);
|
|
}
|
|
const fn = () => {
|
|
this.setState({
|
|
submitting: false,
|
|
});
|
|
};
|
|
this.setState({
|
|
submitting: true,
|
|
});
|
|
callback(fn);
|
|
},
|
|
},
|
|
|
|
render() {
|
|
const { $listeners, $slots } = this;
|
|
const formProps = {
|
|
[formPropName]: this.getForm(),
|
|
};
|
|
const { wrappedComponentRef, ...restProps } = getOptionProps(this);
|
|
const wrappedComponentProps = {
|
|
props: mapProps.call(this, {
|
|
...formProps,
|
|
...restProps,
|
|
}),
|
|
on: $listeners,
|
|
ref: 'WrappedComponent',
|
|
directives: [
|
|
{
|
|
name: 'ant-ref',
|
|
value: wrappedComponentRef,
|
|
},
|
|
],
|
|
};
|
|
|
|
return WrappedComponent ? (
|
|
<WrappedComponent {...wrappedComponentProps}>{$slots.default}</WrappedComponent>
|
|
) : null;
|
|
},
|
|
};
|
|
if (!WrappedComponent) return Form;
|
|
if (Array.isArray(WrappedComponent.props)) {
|
|
const newProps = {};
|
|
WrappedComponent.props.forEach(prop => {
|
|
newProps[prop] = PropTypes.any;
|
|
});
|
|
newProps[formPropName] = Object;
|
|
WrappedComponent.props = newProps;
|
|
} else {
|
|
WrappedComponent.props = WrappedComponent.props || {};
|
|
if (!(formPropName in WrappedComponent.props)) {
|
|
WrappedComponent.props[formPropName] = Object;
|
|
}
|
|
}
|
|
return argumentContainer(Form, WrappedComponent);
|
|
};
|
|
}
|
|
|
|
export default createBaseForm;
|