ant-design-vue/components/form/FormItem.jsx
2018-12-19 21:26:32 +08:00

422 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 ? (
<div class={`${prefixCls}-explain`} key='help'>
{help}
</div>
) : null
if (children) {
this.helpShow = !!children
}
const transitionProps = getTransitionProps('show-help', {
afterEnter: () => this.onHelpAnimEnd('help', true),
afterLeave: () => this.onHelpAnimEnd('help', false),
})
return (
<transition
{...transitionProps}
key='help'
>
{children}
</transition>
)
},
renderExtra () {
const { prefixCls } = this
const extra = getComponentFromProp(this, 'extra')
return extra ? (
<div class={`${prefixCls}-extra`}>{extra}</div>
) : 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)
? <span class={`${props.prefixCls}-item-children-icon`}>
<Icon type={iconType} theme={iconType === 'loading' ? 'outlined' : 'filled'} />
</span> : null
return (
<div class={classes}>
<span class={`${props.prefixCls}-item-children`}>
{c1}{icon}
</span>
{c2}{c3}
</div>
)
},
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 (
<Col {...colProps}>
{children}
</Col>
)
},
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={<a href="xx">link</a>}``
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 ? (
<Col {...colProps} >
<label
for={id || this.getId()}
class={labelClassName}
title={typeof label === 'string' ? label : ''}
onClick={this.onLabelClick}
>
{labelChildren}
</label>
</Col>
) : 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 (
<Row class={classNames(itemClassName)}>
{children}
</Row>
)
},
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)
},
}