import intersperse from 'intersperse'
import PropTypes from '../_util/vue-types'
import classNames from 'classnames'
import find from 'lodash/find'
import Row from '../grid/Row'
import Col, { ColProps } from '../grid/Col'
import warning from '../_util/warning'
import { FIELD_META_PROP, FIELD_DATA_PROP } from './constants'
import { initDefaultProps, getComponentFromProp, filterEmpty, getSlotOptions, isValidElement, getAllChildren } from '../_util/props-util'
import getTransitionProps from '../_util/getTransitionProps'
import BaseMixin from '../_util/BaseMixin'
import { cloneElement, cloneVNodes } from '../_util/vnode'
import Icon from '../icon'
export const FormItemProps = {
id: PropTypes.string,
prefixCls: PropTypes.string,
label: PropTypes.any,
labelCol: PropTypes.shape(ColProps).loose,
wrapperCol: PropTypes.shape(ColProps).loose,
help: PropTypes.any,
extra: PropTypes.any,
validateStatus: PropTypes.oneOf(['', 'success', 'warning', 'error', 'validating']),
hasFeedback: PropTypes.bool,
required: PropTypes.bool,
colon: PropTypes.bool,
fieldDecoratorId: PropTypes.string,
fieldDecoratorOptions: PropTypes.object,
}
export default {
name: 'AFormItem',
__ANT_FORM_ITEM: true,
mixins: [BaseMixin],
props: initDefaultProps(FormItemProps, {
hasFeedback: false,
prefixCls: 'ant-form',
colon: true,
}),
inject: {
FormProps: { default: {}},
decoratorFormProps: { default: {}},
},
data () {
return { helpShow: false }
},
mounted () {
warning(
this.getControls(this.slotDefault, true).length <= 1,
'`Form.Item` cannot generate `validateStatus` and `help` automatically, ' +
'while there are more than one `getFieldDecorator` in it.',
)
warning(
!this.fieldDecoratorId,
'`fieldDecoratorId` is deprecated. please use `v-decorator={id, options}` instead.'
)
},
methods: {
getHelpMessage () {
const help = getComponentFromProp(this, 'help')
const onlyControl = this.getOnlyControl()
if (help === undefined && onlyControl) {
const errors = this.getField().errors
if (errors) {
return intersperse(errors.map((e, index) => {
return isValidElement(e.message) ? cloneElement(e.message, { key: index }) : e.message
}), ' ')
} else {
return ''
}
}
return help
},
getControls (childrenArray = [], recursively) {
let controls = []
for (let i = 0; i < childrenArray.length; i++) {
if (!recursively && controls.length > 0) {
break
}
const child = childrenArray[i]
if (!child.tag && child.text.trim() === '') {
continue
}
if (getSlotOptions(child).__ANT_FORM_ITEM) {
continue
}
const children = getAllChildren(child)
const attrs = child.data && child.data.attrs || {}
if (FIELD_META_PROP in attrs) { // And means FIELD_DATA_PROP in child.props, too.
controls.push(child)
} else if (children) {
controls = controls.concat(this.getControls(children, recursively))
}
}
return controls
},
getOnlyControl () {
const child = this.getControls(this.slotDefault, false)[0]
return child !== undefined ? child : null
},
getChildAttr (prop) {
const child = this.getOnlyControl()
let data = {}
if (!child) {
return undefined
}
if (child.data) {
data = child.data
} else if (child.$vnode && child.$vnode.data) {
data = child.$vnode.data
}
return data[prop] || data.attrs[prop]
},
getId () {
return this.getChildAttr('id')
},
getMeta () {
return this.getChildAttr(FIELD_META_PROP)
},
getField () {
return this.getChildAttr(FIELD_DATA_PROP)
},
onHelpAnimEnd (_key, helpShow) {
this.helpShow = helpShow
if (!helpShow) {
this.$forceUpdate()
}
},
renderHelp () {
const prefixCls = this.prefixCls
const help = this.getHelpMessage()
const children = help ? (
{help}
) : null
if (children) {
this.helpShow = !!children
}
const transitionProps = getTransitionProps('show-help', {
afterEnter: () => this.onHelpAnimEnd('help', true),
afterLeave: () => this.onHelpAnimEnd('help', false),
})
return (
{children}
)
},
renderExtra () {
const { prefixCls } = this
const extra = getComponentFromProp(this, 'extra')
return extra ? (
) : null
},
getValidateStatus () {
const onlyControl = this.getOnlyControl()
if (!onlyControl) {
return ''
}
const field = this.getField()
if (field.validating) {
return 'validating'
}
if (field.errors) {
return 'error'
}
const fieldValue = 'value' in field ? field.value : this.getMeta().initialValue
if (fieldValue !== undefined && fieldValue !== null && fieldValue !== '') {
return 'success'
}
return ''
},
renderValidateWrapper (c1, c2, c3) {
const props = this.$props
const onlyControl = this.getOnlyControl
const validateStatus = (props.validateStatus === undefined && onlyControl)
? this.getValidateStatus()
: props.validateStatus
let classes = `${props.prefixCls}-item-control`
if (validateStatus) {
classes = classNames(`${props.prefixCls}-item-control`, {
'has-feedback': props.hasFeedback || validateStatus === 'validating',
'has-success': validateStatus === 'success',
'has-warning': validateStatus === 'warning',
'has-error': validateStatus === 'error',
'is-validating': validateStatus === 'validating',
})
}
let iconType = ''
switch (validateStatus) {
case 'success':
iconType = 'check-circle'
break
case 'warning':
iconType = 'exclamation-circle'
break
case 'error':
iconType = 'close-circle'
break
case 'validating':
iconType = 'loading'
break
default:
iconType = ''
break
}
const icon = (props.hasFeedback && iconType)
?
: null
return (
{c1}{icon}
{c2}{c3}
)
},
renderWrapper (children) {
const { prefixCls, wrapperCol = {}} = this
const { class: cls, style, id, on, ...restProps } = wrapperCol
const className = classNames(
`${prefixCls}-item-control-wrapper`,
cls,
)
const colProps = {
props: restProps,
class: className,
key: 'wrapper',
style,
id,
on,
}
return (
{children}
)
},
isRequired () {
const { required } = this
if (required !== undefined) {
return required
}
if (this.getOnlyControl()) {
const meta = this.getMeta() || {}
const validate = meta.validate || []
return validate.filter((item) => !!item.rules).some((item) => {
return item.rules.some((rule) => rule.required)
})
}
return false
},
// Resolve duplicated ids bug between different forms
// https://github.com/ant-design/ant-design/issues/7351
onLabelClick (e) {
const label = getComponentFromProp(this, 'label')
const id = this.id || this.getId()
if (!id) {
return
}
const controls = document.querySelectorAll(`[id="${id}"]`)
if (controls.length !== 1) {
// Only prevent in default situation
// Avoid preventing event in `label={link}``
if (typeof label === 'string') {
e.preventDefault()
}
const control = this.$el.querySelector(`[id="${id}"]`)
if (control && control.focus) {
control.focus()
}
}
},
renderLabel () {
const { prefixCls, labelCol = {}, colon, id } = this
const label = getComponentFromProp(this, 'label')
const required = this.isRequired()
const { class: labelColClass, style: labelColStyle, id: labelColId, on, ...restProps } = labelCol
const labelColClassName = classNames(
`${prefixCls}-item-label`,
labelColClass,
)
const labelClassName = classNames({
[`${prefixCls}-item-required`]: required,
})
let labelChildren = label
// Keep label is original where there should have no colon
const haveColon = colon && this.FormProps.layout !== 'vertical'
// Remove duplicated user input colon
if (haveColon && typeof label === 'string' && label.trim() !== '') {
labelChildren = label.replace(/[:|:]\s*$/, '')
}
const colProps = {
props: restProps,
class: labelColClassName,
key: 'label',
style: labelColStyle,
id: labelColId,
on,
}
return label ? (
) : null
},
renderChildren () {
return [
this.renderLabel(),
this.renderWrapper(
this.renderValidateWrapper(
this.slotDefault,
this.renderHelp(),
this.renderExtra(),
),
),
]
},
renderFormItem (children) {
const props = this.$props
const prefixCls = props.prefixCls
const itemClassName = {
[`${prefixCls}-item`]: true,
[`${prefixCls}-item-with-help`]: this.helpShow,
[`${prefixCls}-item-no-colon`]: !props.colon,
}
return (
{children}
)
},
decoratorOption (vnode) {
if (vnode.data && vnode.data.directives) {
const directive = find(vnode.data.directives, ['name', 'decorator'])
warning(
!directive || (directive && Array.isArray(directive.value)),
`Invalid directive: type check failed for directive "decorator". Expected Array, got ${typeof directive.value}. At ${vnode.tag}.`,
)
return directive ? directive.value : null
} else {
return null
}
},
decoratorChildren (vnodes) {
const { FormProps } = this
const getFieldDecorator = FormProps.form.getFieldDecorator
vnodes.forEach((vnode, index) => {
if (vnode.children) {
vnode.children = this.decoratorChildren(cloneVNodes(vnode.children))
} else if (vnode.componentOptions && vnode.componentOptions.children) {
vnode.componentOptions.children = this.decoratorChildren(cloneVNodes(vnode.componentOptions.children))
}
const option = this.decoratorOption(vnode)
if (option && option[0]) {
vnodes[index] = getFieldDecorator(option[0], option[1])(vnode)
}
})
return vnodes
},
},
render () {
const { $slots, decoratorFormProps, fieldDecoratorId, fieldDecoratorOptions = {}, FormProps } = this
let child = filterEmpty($slots.default || [])
if (decoratorFormProps.form && fieldDecoratorId && child.length) {
const getFieldDecorator = decoratorFormProps.form.getFieldDecorator
child[0] = getFieldDecorator(fieldDecoratorId, fieldDecoratorOptions)(child[0])
warning(
!(child.length > 1),
'`autoFormCreate` just `decorator` then first children. but you can use JSX to support multiple children',
)
this.slotDefault = child
} else if (FormProps.form) {
child = cloneVNodes(child)
this.slotDefault = this.decoratorChildren(child)
} else {
this.slotDefault = child
}
const children = this.renderChildren()
return this.renderFormItem(children)
},
}