zent
Version:
一套前端设计语言和基于React的实现
607 lines (531 loc) • 17.8 kB
JavaScript
/* eslint-disable no-underscore-dangle */
import { Component, PureComponent, createElement } from 'react';
import omit from 'lodash/omit';
import find from 'lodash/find';
import noop from 'lodash/noop';
import assign from 'lodash/assign';
import isEqual from 'lodash/isEqual';
import some from 'lodash/some';
import get from 'lodash/get';
import isPromise from 'utils/isPromise';
import PropTypes from 'prop-types';
import { getDisplayName, silenceEvent, silenceEvents } from './utils';
import rules from './validationRules';
import handleSubmit from './handleSubmit';
const emptyArray = [];
const checkSubmit = submit => {
if (!submit || typeof submit !== 'function') {
throw new Error(
'You must either pass handleSubmit() an onSubmit function or pass onSubmit as a prop'
);
}
return submit;
};
const createForm = (config = {}) => {
const { formValidations } = config;
const validationRules = assign({}, rules, formValidations);
return WrappedForm => {
return class Form extends (PureComponent || Component) {
constructor(props) {
super(props);
this.state = {
isFormValid: true,
isSubmitting: false
};
this.fields = [];
this._isMounted = false;
}
static displayName = `Form(${getDisplayName(WrappedForm)})`;
static WrappedForm = WrappedForm;
static propTypes = {
onSubmit: PropTypes.func,
onSubmitSuccess: PropTypes.func,
onSubmitFail: PropTypes.func,
onValid: PropTypes.func,
onInvalid: PropTypes.func,
onChange: PropTypes.func,
validationErrors: PropTypes.object
};
static defaultProps = {
onSubmit: noop,
onSubmitSuccess: noop,
onSubmitFail: noop,
onValid: noop,
onInvalid: noop,
onChange: noop,
validationErrors: null
};
static childContextTypes = {
zentForm: PropTypes.object
};
getChildContext() {
return {
zentForm: {
attachToForm: this.attachToForm,
detachFromForm: this.detachFromForm,
validate: this.validate,
asyncValidate: this.asyncValidate,
getFormValues: this.getFormValues,
getFieldError: this.getFieldError,
isValidValue: this.isValidValue,
setFieldExternalErrors: this.setFieldExternalErrors,
resetFieldsValue: this.resetFieldsValue,
setFormDirty: this.setFormDirty,
setFormPristine: this.setFormDirty,
isValid: this.isValid,
isSubmitting: this.isSubmitting
}
};
}
componentDidMount() {
this.validateForm();
this._isMounted = true;
}
componentWillUpdate() {
this.prevFieldNames = this.fields.map(field => field.getName());
}
componentDidUpdate() {
const { validationErrors } = this.props;
if (
validationErrors &&
typeof validationErrors === 'object' &&
Object.keys(validationErrors).length > 0
) {
this.setFieldValidationErrors(validationErrors);
}
const newFieldNames = this.fields.map(field => field.getName());
if (!isEqual(this.prevFieldNames, newFieldNames)) {
this.validateForm();
}
}
submitCompleted = result => {
delete this.submitPromise;
return result;
};
submitFailed = error => {
delete this.submitPromise;
throw error;
};
listenToSubmit = promise => {
if (!isPromise(promise)) {
return promise;
}
// 当submit是一个promise时,需要一个标识表明正在提交,提交结束后删除标识
this.submitPromise = promise;
return promise.then(this.submitCompleted, this.submitFailed);
};
submit = submitOrEvent => {
const { onSubmit } = this.props;
// 在表单中手动调用handleSubmit或者把handleSubmit赋值给表单的onSubmit回调
// handleSubmit赋值给表单的onSubmit时,submitOrEvent是一个event对象
// handleSubmit的参数必须是function
if (!submitOrEvent || silenceEvent(submitOrEvent)) {
if (!this.submitPromise) {
// 调用props传入的onSubmit方法
return this.listenToSubmit(
handleSubmit(checkSubmit(onSubmit), this)
);
}
} else {
// submitOrEvent是一个自定义submit函数,返回一个promise对象
return silenceEvents(
() =>
!this.submitPromise &&
this.listenToSubmit(
handleSubmit(checkSubmit(submitOrEvent), this)
)
);
}
};
isSubmitting = () => {
return this.state.isSubmitting;
};
isValid = () => {
return this.state.isFormValid;
};
setFieldValidationErrors = (errors, updateDirty = true) => {
this.fields.forEach(field => {
const name = field.getName();
const data = {
_isValid: !(name in errors),
_validationError:
typeof errors[name] === 'string' ? [errors[name]] : errors[name]
};
if (updateDirty) {
data._isDirty = true;
}
field.setState(data);
});
};
// 设置服务端返回的错误信息
setFieldExternalErrors = (errors, updateDirty = true) => {
this.fields.forEach(field => {
const name = field.getName();
const error = get(errors, name);
const data = {
_isValid: false,
_externalError: typeof error === 'string' ? [error] : error
};
if (updateDirty) {
data._isDirty = true;
}
field.setState(data);
});
};
setFormDirty = isDirty => {
this.fields.forEach(field => {
field.setState({
_isDirty: isDirty
});
});
};
setFormPristine = isPristine => {
this.fields.forEach(field => {
field.setState({
_isDirty: !isPristine
});
});
};
initialize = data => {
this.fields.forEach(field => {
const name = field.getName();
const value = get(data, name);
if (value) {
field.setInitialValue(value);
} else {
field.setInitialValue();
}
});
};
resetFieldsValue = data => {
this.fields.forEach(field => {
const name = field.getName();
const value = get(data, name);
if (value !== undefined) {
field.setValue(value);
} else {
field.resetValue();
}
});
};
setFieldsValue = data => {
this.fields.forEach(field => {
const name = field.getName();
const value = get(data, name);
if (value) {
field.setValue(value);
}
});
};
reset = data => {
this.setFormDirty(false);
this.resetFieldsValue(data);
};
isFieldDirty = name => {
const field = find(
this.fields,
component => component.getName() === name
);
if (!field) return false;
return field.isDirty();
};
isFieldValidating = name => {
const field = find(
this.fields,
component => component.getName() === name
);
if (!field) return false;
return field.isValidating();
};
getFieldError = name => {
const field = find(
this.fields,
component => component.getName() === name
);
if (!field) return '';
return field.getErrorMessage();
};
getFormValues = () => {
const assignValue = (values, keyPath, newValue) => {
if (keyPath.length === 0) {
return;
}
let currentKey = keyPath[0];
if (/\[\d+\]/.test(currentKey)) {
// array
let index = currentKey.match(/\d+(?=\])/)[0];
currentKey = currentKey.replace(/\[\d+\]/, '');
if (!values[currentKey]) {
values[currentKey] = [];
}
if (keyPath.length > 1) {
index > values[currentKey].length - 1
? values[currentKey].push({})
: null;
assignValue(
values[currentKey][index],
keyPath.slice(1),
newValue
);
} else {
values[currentKey][index] = newValue;
}
} else {
// object
if (!values[currentKey]) {
values[currentKey] = {};
}
if (keyPath.length > 1) {
assignValue(values[currentKey], keyPath.slice(1), newValue);
} else {
values[currentKey] = newValue;
}
}
};
return this.fields.reduce((values, field) => {
const name = field.getName();
const fieldValue = field.getValue();
const fieldNamePath = name.split('.');
assignValue(values, fieldNamePath, fieldValue);
return values;
}, {});
};
getValidationErrors = () => {
return this.fields.reduce((errors, field) => {
const name = field.getName();
errors[name] = field.getErrorMessage();
return errors;
}, {});
};
getInitialValues = () => {
return this.fields.reduce((values, field) => {
const name = field.getName();
values[name] = field.getInitialValue();
return values;
}, {});
};
isChanged = () => {
return !isEqual(this.getInitialValues(), this.getFormValues());
};
isValidating = () => {
return some(this.fields, field => {
return field.isValidating();
});
};
isValidValue = (field, value) => {
return this.runValidation(field, value).isValid;
};
runValidation = (field, value = field.getValue()) => {
const formValidationErrors = this.props.validationErrors;
const { name, validationError, validationErrors } = field.props;
const currentValues = this.getFormValues();
const validationResults = this.runRules(
value,
currentValues,
field._validations
);
const isValid =
!validationResults.failed.length &&
!(formValidationErrors && formValidationErrors[field.getName()]);
return {
isValid,
error: (function() {
if (isValid) {
return emptyArray;
}
if (validationResults.errors.length) {
return validationResults.errors;
}
if (formValidationErrors && formValidationErrors[name]) {
return typeof formValidationErrors[name] === 'string'
? [formValidationErrors[name]]
: formValidationErrors[name];
}
if (validationResults.failed.length) {
return validationResults.failed
.map(failed => {
return validationErrors[failed]
? validationErrors[failed]
: validationError;
})
.filter((x, pos, arr) => {
// Remove duplicates
return arr.indexOf(x) === pos;
});
}
})()
};
};
runRules = (value, currentValues, validations = []) => {
const results = {
errors: [],
failed: []
};
function updateResults(validation, validationMethod) {
// validation方法可以直接返回错误信息,否则需要返回布尔值表明校验是否成功
if (typeof validation === 'string') {
results.errors.push(validation);
results.failed.push(validationMethod);
} else if (!validation) {
results.failed.push(validationMethod);
}
}
Object.keys(validations).forEach(validationMethod => {
// validations中不指定function则必须是内置的rule
if (
!validationRules[validationMethod] &&
typeof validations[validationMethod] !== 'function'
) {
throw new Error(
`does not have the validation rule: ${validationMethod}`
);
}
// 使用自定义校验方法或内置校验方法(可以按需添加)
if (typeof validations[validationMethod] === 'function') {
const validation = validations[validationMethod](
currentValues,
value
);
updateResults(validation, validationMethod);
} else {
const validation = validationRules[validationMethod](
currentValues,
value,
validations[validationMethod]
);
updateResults(validation, validationMethod);
}
});
return results;
};
onValidationComplete = () => {
const allIsValid = this.fields.every(field => {
return field.isValid();
});
this.setState({
isFormValid: allIsValid
});
if (allIsValid) {
this.props.onValid();
} else {
this.props.onInvalid();
}
};
validate = field => {
// 初始化时调用validate不触发onChange
if (this._isMounted) {
this.props.onChange(this.getFormValues(), this.isChanged());
}
const validation = this.runValidation(field);
field.setState(
{
_isValid: validation.isValid,
_validationError: validation.error,
_externalError: null
},
this.validateForm
);
};
asyncValidate = (field, value) => {
const { asyncValidation } = field.props;
const values = this.getFormValues();
if (!asyncValidation && field.state._validationError.length) return;
field.setState({
_isValidating: true
});
const promise = asyncValidation(values, value);
if (!isPromise(promise)) {
throw new Error('asyncValidation function must return a promise');
}
const handleResult = rejected => error => {
field.setState({
_isValidating: false,
_isValid: !rejected && field.state._validationError.length === 0,
_externalError: error ? [error] : null,
_asyncValidated: true
});
if (rejected) {
this.setState({
isFormValid: false
});
throw new Error(error);
}
};
return promise.then(handleResult(false), handleResult(true));
};
isFormAsyncValidated = () => {
const allIsAsyncValid = this.fields.every(field => {
return field.isAsyncValidated() || !field.props.asyncValidation;
});
return allIsAsyncValid;
};
validateForm = () => {
this.fields.forEach((field, index) => {
const { _externalError } = field.state;
const validation = this.runValidation(field);
if (validation.isValid && _externalError) {
validation.isValid = false;
}
field.setState(
{
_isValid: validation.isValid,
_validationError: validation.error,
_externalError:
!validation.isValid && _externalError ? _externalError : null
},
index === this.fields.length - 1 ? this.onValidationComplete : null
);
});
};
attachToForm = field => {
if (this.fields.indexOf(field) < 0) {
this.fields.push(field);
}
// form初始化时不校验,后续动态添加的元素再校验
this._isMounted && this.validate(field);
};
detachFromForm = field => {
const fieldPos = this.fields.indexOf(field);
if (fieldPos >= 0) {
this.fields.splice(fieldPos, 1);
}
this.validateForm();
};
getWrappedForm = () => {
return this.wrappedForm;
};
render() {
const passableProps = omit(this.props, [
'validationErrors',
'handleSubmit',
'onChange'
]);
return createElement(WrappedForm, {
...passableProps,
ref: ref => {
this.wrappedForm = ref;
},
handleSubmit: this.submit,
zentForm: {
getFormValues: this.getFormValues,
getFieldError: this.getFieldError,
setFieldExternalErrors: this.setFieldExternalErrors,
resetFieldsValue: this.resetFieldsValue,
setFieldsValue: this.setFieldsValue,
setFormDirty: this.setFormDirty,
setFormPristine: this.setFormPristine,
initialize: this.initialize,
isFieldDirty: this.isFieldDirty,
isFieldTouched: this.isFieldDirty,
isFieldValidating: this.isFieldValidating,
isValid: this.isValid,
isValidating: this.isValidating,
isSubmitting: this.isSubmitting,
isFormAsyncValidated: this.isFormAsyncValidated
}
});
}
};
};
};
export default createForm;