ant-design-vue/components/vc-form/src/createBaseForm.jsx

601 lines
20 KiB
React
Raw Normal View History

2018-05-02 13:35:42 +00:00
import AsyncValidator from 'async-validator'
import warning from 'warning'
import get from 'lodash/get'
import set from 'lodash/set'
2018-05-05 09:00:51 +00:00
import omit from 'lodash/omit'
2018-05-02 13:35:42 +00:00
import createFieldsStore from './createFieldsStore'
import { cloneElement } from '../../_util/vnode'
2018-05-03 11:10:43 +00:00
import BaseMixin from '../../_util/BaseMixin'
2018-05-04 04:16:17 +00:00
import { getOptionProps, getEvents } from '../../_util/props-util'
2018-05-06 10:32:40 +00:00
import PropTypes from '../../_util/vue-types'
2018-05-04 04:16:17 +00:00
2018-05-02 13:35:42 +00:00
import {
argumentContainer,
identity,
normalizeValidateRules,
getValidateTriggers,
getValueFromEvent,
hasRules,
getParams,
isEmptyObject,
flattenArray,
} from './utils'
2018-05-04 08:02:31 +00:00
const DEFAULT_TRIGGER = 'change'
2018-05-02 13:35:42 +00:00
function createBaseForm (option = {}, mixins = []) {
const {
validateMessages,
onFieldsChange,
onValuesChange,
mapProps = identity,
mapPropsToFields,
fieldNameProp,
fieldMetaProp,
fieldDataProp,
formPropName = 'form',
2018-05-06 10:32:40 +00:00
props = {},
2018-06-23 09:17:45 +00:00
templateContext,
2018-05-02 13:35:42 +00:00
} = option
return function decorate (WrappedComponent) {
2018-05-06 10:32:40 +00:00
let formProps = {}
if (Array.isArray(props)) {
props.forEach((prop) => {
formProps[prop] = PropTypes.any
})
} else {
formProps = props
}
2018-05-03 11:10:43 +00:00
const Form = {
mixins: [BaseMixin, ...mixins],
2018-05-06 10:32:40 +00:00
props: {
...formProps,
wrappedComponentRef: PropTypes.func.def(() => {}),
},
2018-05-03 11:10:43 +00:00
data () {
const fields = mapPropsToFields && mapPropsToFields(this.$props)
2018-05-02 13:35:42 +00:00
this.fieldsStore = createFieldsStore(fields || {})
this.instances = {}
this.cachedBind = {}
this.clearedFieldMetaCache = {};
// 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,
}
},
2018-05-03 11:10:43 +00:00
watch: {
'$props': {
handler: function (nextProps) {
if (mapPropsToFields) {
this.fieldsStore.updateFields(mapPropsToFields(nextProps))
}
},
deep: true,
},
2018-05-02 13:35:42 +00:00
},
2018-05-06 10:32:40 +00:00
mounted () {
this.wrappedComponentRef(this.$refs.WrappedComponent)
},
updated () {
this.wrappedComponentRef(this.$refs.WrappedComponent)
},
destroyed () {
this.wrappedComponentRef(null)
},
2018-05-03 11:10:43 +00:00
methods: {
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]))
2018-05-04 11:11:42 +00:00
onValuesChange(this, set({}, name, value), valuesAllSet)
2018-05-03 11:10:43 +00:00
}
const field = this.fieldsStore.getField(name)
return ({ name, field: { ...field, value, touched: true }, fieldMeta })
},
2018-05-02 13:35:42 +00:00
2018-05-03 11:10:43 +00:00
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,
})
},
2018-05-02 13:35:42 +00:00
2018-05-03 11:10:43 +00:00
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,
},
})
},
2018-05-02 13:35:42 +00:00
2018-05-03 11:10:43 +00:00
getCacheBind (name, action, fn) {
if (!this.cachedBind[name]) {
this.cachedBind[name] = {}
}
const cache = this.cachedBind[name]
if (!cache[action]) {
cache[action] = fn.bind(this, name, action)
}
return cache[action]
},
2018-05-02 13:35:42 +00:00
2018-05-03 11:10:43 +00:00
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]
}
},
2018-05-02 13:35:42 +00:00
2018-05-03 11:10:43 +00:00
getFieldDecorator (name, fieldOption) {
2018-05-04 04:16:17 +00:00
const { props, ...restProps } = this.getFieldProps(name, fieldOption)
2018-05-03 11:10:43 +00:00
return (fieldElem) => {
const fieldMeta = this.fieldsStore.getFieldMeta(name)
2018-05-04 04:16:17 +00:00
const originalProps = getOptionProps(fieldElem)
const originalEvents = getEvents(fieldElem)
2018-05-03 11:10:43 +00:00
if (process.env.NODE_ENV !== 'production') {
const valuePropName = fieldMeta.valuePropName
warning(
!(valuePropName in originalProps),
`\`getFieldDecorator\` will override \`${valuePropName}\`, ` +
`so please don't set \`${valuePropName}\` 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
2018-05-04 04:16:17 +00:00
// fieldMeta.ref = fieldElem.data && fieldElem.data.ref
const newProps = {
2018-05-03 11:10:43 +00:00
props: {
2018-05-03 14:53:17 +00:00
...props,
2018-05-03 11:10:43 +00:00
...this.fieldsStore.getFieldValuePropValue(fieldMeta),
},
2018-05-04 04:16:17 +00:00
...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)
}
2018-05-05 09:00:51 +00:00
} else {
newEvents[key] = newProps.on[key]
2018-05-04 04:16:17 +00:00
}
2018-05-03 11:10:43 +00:00
})
2018-05-04 04:16:17 +00:00
return cloneElement(fieldElem, { ...newProps, on: newEvents })
2018-05-03 11:10:43 +00:00
}
},
2018-05-02 13:35:42 +00:00
2018-05-03 11:10:43 +00:00
getFieldProps (name, usersFieldOption = {}) {
if (!name) {
throw new Error('Must call `getFieldProps` with valid name string!')
}
2018-05-02 13:35:42 +00:00
if (process.env.NODE_ENV !== 'production') {
warning(
2018-05-03 11:10:43 +00:00
this.fieldsStore.isValidNestedFieldName(name),
'One field name cannot be part of another, e.g. `a` and `a.b`.'
2018-05-02 13:35:42 +00:00
)
warning(
2018-05-03 11:10:43 +00:00
!('exclusive' in usersFieldOption),
'`option.exclusive` of `getFieldProps`|`getFieldDecorator` had been remove.'
2018-05-02 13:35:42 +00:00
)
}
2018-05-03 11:10:43 +00:00
delete this.clearedFieldMetaCache[name]
2018-05-02 13:35:42 +00:00
2018-05-03 11:10:43 +00:00
const fieldOption = {
name,
trigger: DEFAULT_TRIGGER,
valuePropName: 'value',
validate: [],
...usersFieldOption,
}
2018-05-02 13:35:42 +00:00
2018-05-03 11:10:43 +00:00
const {
rules,
trigger,
validateTrigger = trigger,
validate,
} = fieldOption
2018-05-02 13:35:42 +00:00
2018-05-03 11:10:43 +00:00
const fieldMeta = this.fieldsStore.getFieldMeta(name)
if ('initialValue' in fieldOption) {
fieldMeta.initialValue = fieldOption.initialValue
}
2018-05-02 13:35:42 +00:00
2018-05-03 11:10:43 +00:00
const inputProps = {
...this.fieldsStore.getFieldValuePropValue(fieldOption),
2018-05-03 14:53:17 +00:00
// ref: name,
2018-05-03 11:10:43 +00:00
}
2018-05-04 04:16:17 +00:00
const inputListeners = {}
2018-05-05 09:00:51 +00:00
const inputAttrs = {}
2018-05-03 11:10:43 +00:00
if (fieldNameProp) {
inputProps[fieldNameProp] = name
}
2018-05-02 13:35:42 +00:00
2018-05-03 11:10:43 +00:00
const validateRules = normalizeValidateRules(validate, rules, validateTrigger)
const validateTriggers = getValidateTriggers(validateRules)
validateTriggers.forEach((action) => {
2018-05-04 04:16:17 +00:00
if (inputListeners[action]) return
inputListeners[action] = this.getCacheBind(name, action, this.onCollectValidate)
2018-05-03 11:10:43 +00:00
})
2018-05-02 13:35:42 +00:00
2018-05-03 11:10:43 +00:00
// make sure that the value will be collect
if (trigger && validateTriggers.indexOf(trigger) === -1) {
2018-05-04 04:16:17 +00:00
inputListeners[trigger] = this.getCacheBind(name, trigger, this.onCollect)
2018-05-03 11:10:43 +00:00
}
2018-05-02 13:35:42 +00:00
2018-05-03 11:10:43 +00:00
const meta = {
...fieldMeta,
...fieldOption,
validate: validateRules,
}
this.fieldsStore.setFieldMeta(name, meta)
if (fieldMetaProp) {
2018-05-05 09:00:51 +00:00
inputAttrs[fieldMetaProp] = meta
2018-05-03 11:10:43 +00:00
}
2018-05-02 13:35:42 +00:00
2018-05-03 11:10:43 +00:00
if (fieldDataProp) {
2018-05-05 09:00:51 +00:00
inputAttrs[fieldDataProp] = this.fieldsStore.getField(name)
2018-05-03 11:10:43 +00:00
}
2018-05-02 13:35:42 +00:00
2018-05-03 14:53:17 +00:00
return {
2018-05-05 09:00:51 +00:00
props: omit(inputProps, ['id']),
// id: inputProps.id,
2018-05-04 04:16:17 +00:00
domProps: {
value: inputProps.value,
},
2018-05-05 09:00:51 +00:00
attrs: {
...inputAttrs,
id: inputProps.id,
},
2018-05-03 14:53:17 +00:00
directives: [
2018-05-04 04:16:17 +00:00
{
name: 'ant-ref',
value: this.getCacheBind(name, `${name}__ref`, this.saveRef),
},
2018-05-03 14:53:17 +00:00
],
2018-05-04 04:16:17 +00:00
on: inputListeners,
2018-05-03 14:53:17 +00:00
}
2018-05-03 11:10:43 +00:00
},
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)), {})
2018-05-04 11:11:42 +00:00
onFieldsChange(this, changedFields, this.fieldsStore.getNestedAllFields())
2018-05-03 11:10:43 +00:00
}
2018-06-23 09:17:45 +00:00
if (templateContext) {
templateContext.$forceUpdate()
} else {
this.$forceUpdate()
}
2018-05-03 14:53:17 +00:00
this.$nextTick(() => {
callback && callback()
})
2018-05-03 11:10:43 +00:00
},
2018-05-02 13:35:42 +00:00
2018-05-03 11:10:43 +00:00
resetFields (ns) {
const newFields = this.fieldsStore.resetFields(ns)
if (Object.keys(newFields).length > 0) {
this.setFields(newFields)
2018-05-02 13:35:42 +00:00
}
2018-05-03 11:10:43 +00:00
if (ns) {
const names = Array.isArray(ns) ? ns : [ns]
names.forEach(name => delete this.clearedFieldMetaCache[name])
} else {
this.clearedFieldMetaCache = {}
2018-05-02 13:35:42 +00:00
}
2018-05-03 11:10:43 +00:00
},
2018-05-02 13:35:42 +00:00
2018-05-03 11:10:43 +00:00
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()
2018-05-04 11:11:42 +00:00
onValuesChange(this, changedValues, allValues)
2018-05-02 13:35:42 +00:00
}
2018-05-03 11:10:43 +00:00
},
saveRef (name, _, component) {
2018-05-03 14:53:17 +00:00
if (!component) {
// after destroy, delete data
this.clearedFieldMetaCache[name] = {
field: this.fieldsStore.getField(name),
meta: this.fieldsStore.getFieldMeta(name),
}
this.fieldsStore.clearField(name)
delete this.instances[name]
delete this.cachedBind[name]
return
}
2018-05-03 11:10:43 +00:00
this.recoverClearedField(name)
2018-05-04 04:16:17 +00:00
// 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)
// }
// }
2018-05-03 11:10:43 +00:00
this.instances[name] = component
},
2018-05-02 13:35:42 +00:00
2018-05-03 11:10:43 +00:00
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
2018-05-02 13:35:42 +00:00
}
2018-05-03 11:10:43 +00:00
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))
2018-05-02 13:35:42 +00:00
return
}
2018-05-03 11:10:43 +00:00
const validator = new AsyncValidator(allRules)
if (validateMessages) {
validator.messages(validateMessages)
2018-05-02 13:35:42 +00:00
}
2018-05-03 11:10:43 +00:00
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
2018-05-02 13:35:42 +00:00
}
})
2018-05-03 11:10:43 +00:00
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))
2018-05-02 13:35:42 +00:00
}
})
2018-05-03 11:10:43 +00:00
},
validateFields (ns, opt, cb) {
const { names, callback, options } = getParams(ns, opt, cb)
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))
2018-05-02 13:35:42 +00:00
}
2018-05-03 11:10:43 +00:00
return
2018-05-02 13:35:42 +00:00
}
2018-05-03 11:10:43 +00:00
if (!('firstFields' in options)) {
options.firstFields = fieldNames.filter((name) => {
const fieldMeta = this.fieldsStore.getFieldMeta(name)
return !!fieldMeta.validateFirst
})
2018-05-02 13:35:42 +00:00
}
2018-05-03 11:10:43 +00:00
this.validateFieldsInternal(fields, {
fieldNames,
options,
}, callback)
},
2018-05-02 13:35:42 +00:00
2018-05-03 11:10:43 +00:00
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
},
2018-05-02 13:35:42 +00:00
2018-05-03 11:10:43 +00:00
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,
})
}
2018-05-02 13:35:42 +00:00
this.setState({
2018-05-03 11:10:43 +00:00
submitting: true,
2018-05-02 13:35:42 +00:00
})
2018-05-03 11:10:43 +00:00
callback(fn)
},
2018-05-02 13:35:42 +00:00
},
render () {
2018-05-07 10:40:25 +00:00
const { $listeners, $slots } = this
2018-05-02 13:35:42 +00:00
const formProps = {
[formPropName]: this.getForm(),
}
2018-05-04 08:02:31 +00:00
const props = getOptionProps(this)
2018-05-03 11:10:43 +00:00
const wrappedComponentProps = {
props: mapProps.call(this, {
...formProps,
2018-05-04 08:02:31 +00:00
...props,
2018-05-03 11:10:43 +00:00
}),
on: $listeners,
2018-05-06 10:32:40 +00:00
ref: 'WrappedComponent',
2018-05-02 13:35:42 +00:00
}
2018-05-07 10:40:25 +00:00
return <WrappedComponent {...wrappedComponentProps}>{$slots.default}</WrappedComponent>
2018-05-02 13:35:42 +00:00
},
2018-05-03 11:10:43 +00:00
}
2018-05-06 10:32:40 +00:00
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
2018-05-04 08:02:31 +00:00
}
}
2018-05-02 13:35:42 +00:00
return argumentContainer(Form, WrappedComponent)
}
}
export default createBaseForm