Merge branch 'master' into unit-test

# Conflicts:
#	package-lock.json
#	package.json
This commit is contained in:
tangjinzhou 2018-05-12 11:14:22 +08:00
commit 0c11e6baca
99 changed files with 7098 additions and 108 deletions

1
.gitignore vendored
View File

@ -55,6 +55,7 @@ typings/
.yarn-integrity
# dotenv environment variables file
.vscode
.env
.idea
.DS_Store

42
.vscode/settings.json vendored
View File

@ -1,42 +0,0 @@
//
{
"eslint.enable": true,
"eslint.options": {
"extensions": [
".js",
".jsx",
".vue"
],
"configFile": ".eslintrc"
},
"eslint.validate": [
{
"language": "html",
"autoFix": true
},
{
"language": "vue",
"autoFix": true
},
{
"language": "javascript",
"autoFix": true
},
{
"language": "javascriptreact",
"autoFix": true
}
],
"emmet.syntaxProfiles": {
"vue-html": "html",
"vue": "html"
},
"eslint.autoFixOnSave": true,
"vetur.validation.template": true,
"vetur.format.html.wrap_line_length": 60,
"vetur.format.js.InsertSpaceBeforeFunctionParenthesis": true,
"stylefmt.config": {
"fix": true
},
"editor.tabSize": 2
}

View File

@ -2,6 +2,33 @@
---
## 0.5.1
`2018-05-10`
- 🐞 `Table`: Fix `customRow` events not working[#16](https://github.com/vueComponent/ant-design/issues/16)
## 0.5.0
`2018-05-08`
- 🌟 `Form`: add Form component
- 💄 `Upload.Dragger`: Modify `name` name to `a-upload-dragger`
- 🐞 `Upload`: Fix `name` prop not working
## 0.4.3
`2018-05-02`
- 🐞 Fix component style loss problem
- 🌟 site add babel-polyfill
## 0.4.2
`2018-04-24`
- 🐞 fix menu click bug
## 0.4.1
#### bug

View File

@ -2,6 +2,34 @@
---
## 0.5.1
`2018-05-10`
- 🐞 `Table`: 修复 `customRow` 自定义事件不生效问题[#16](https://github.com/vueComponent/ant-design/issues/16)
## 0.5.0
`2018-05-08`
- 🌟 `Form `新增Form表单组件
- 💄 `Upload.Dragger`: 修改组件name名称为`a-upload-dragger`
- 🐞 `Upload`: 修复Upload name属性失效问题
## 0.4.3
`2018-05-02`
- 🐞 修复组件样式丢失问题
- 🌟 站点添加babel-polyfill
## 0.4.2
`2018-04-24`
- 🐞 修复menu 非 inline 模式下的 click bug
## 0.4.1
#### bug

View File

@ -13,6 +13,7 @@
Ant Design 3.X 的 Vue 实现,开发和服务于企业级后台产品。
[官网国内镜像](http://tangjinzhou.gitee.io/ant-design/docs/vue/introduce-cn/)
[README in English](README.md)

View File

@ -14,12 +14,14 @@
An enterprise-class UI components based on Ant Design 3.X and Vue.
[官网国内镜像](http://tangjinzhou.gitee.io/ant-design/docs/vue/introduce-cn/)
[中文 README](README-zh_CN.md)
## Features
- An enterprise-class UI design system for desktop applications.
- A set of high-quality React components out of the box.
- A set of high-quality Vue components out of the box.
- Shared [Ant Design of React](https://ant.design/docs/spec/introduce) design resources.

View File

@ -7,6 +7,7 @@ module.exports = function (modules) {
require.resolve('babel-plugin-transform-es3-property-literals'),
require.resolve('babel-plugin-transform-object-assign'),
require.resolve('babel-plugin-transform-object-rest-spread'),
require.resolve('babel-plugin-transform-class-properties'),
]
plugins.push([require.resolve('babel-plugin-transform-runtime'), {
polyfill: false,

View File

@ -78,12 +78,12 @@ function babelify (js, modules) {
let stream = js.pipe(babel(babelConfig))
.pipe(through2.obj(function z (file, encoding, next) {
this.push(file.clone())
if (file.path.match(/\/style\/index\.js/)) {
if (file.path.match(/\/style\/index\.(js|jsx)$/)) {
const content = file.contents.toString(encoding)
file.contents = Buffer.from(content
.replace(/\/style\/?'/g, '/style/css\'')
.replace(/\.less/g, '.css'))
file.path = file.path.replace(/index\.js/, 'css.js')
file.path = file.path.replace(/index\.(js|jsx)$/, 'css.js')
this.push(file)
next()
} else {
@ -238,15 +238,25 @@ function pub (done) {
gulp.task('dist', ['compile'], (done) => {
dist(done)
})
gulp.task('compile', ['compile-with-es'], () => {
gulp.task('compile', ['compile-with-es'], (done) => {
compile()
.on('finish', function () {
done()
})
})
gulp.task('compile-with-es', () => {
gulp.task('compile-with-es', (done) => {
compile(false)
.on('finish', function () {
done()
})
})
gulp.task('pub', ['check-git', 'compile'], (done) => {
if (!process.env.GITHUB_TOKEN) {
console.log('no GitHub token found, skip')
} else {
pub(done)
}
})
function reportError () {

View File

@ -1,6 +1,8 @@
import Vue from 'vue'
import PropTypes from './vue-types'
import antRefDirective from './antRefDirective'
Vue.use(antRefDirective)
export default {
props: {

View File

@ -0,0 +1,12 @@
export default {
install: (Vue, options) => {
Vue.directive('ant-ref', {
bind: function (el, binding, vnode) {
binding.value(vnode)
},
unbind: function (el, binding, vnode) {
binding.value()
},
})
},
}

View File

@ -1,5 +1,10 @@
import isPlainObject from 'lodash/isPlainObject'
function getType (fn) {
const match = fn && fn.toString().match(/^\s*function (\w+)/)
return match ? match[1] : ''
}
const camelizeRE = /-(\w)/g
const camelize = (str) => {
return str.replace(camelizeRE, (_, c) => c ? c.toUpperCase() : '')
@ -40,9 +45,9 @@ const filterProps = (props, propsData = {}) => {
return res
}
const getSlots = (ele) => {
let componentOptions = ele.componentOptions
let componentOptions = ele.componentOptions || {}
if (ele.$vnode) {
componentOptions = ele.$vnode.componentOptions
componentOptions = ele.$vnode.componentOptions || {}
}
const children = componentOptions.children || []
const slots = {}
@ -67,8 +72,11 @@ const getOptionProps = (instance) => {
const props = (Ctor.options || {}).props || {}
const res = {}
for (const [k, v] of Object.entries(props)) {
if (v.default !== undefined) {
res[k] = typeof v.default === 'function' ? v.default() : v.default
const def = v.default
if (def !== undefined) {
res[k] = typeof def === 'function' && getType(v.type) !== 'Function'
? def.call(instance)
: def
}
}
return { ...res, ...propsData }

View File

@ -60,7 +60,7 @@ export function cloneElement (n, nodeProps, deep) {
return null
}
const node = cloneVNode(ele, deep)
const { props = {}, key, on = {}, children } = nodeProps
const { props = {}, key, on = {}, children, directives = [] } = nodeProps
const data = node.data || {}
let cls = {}
let style = {}
@ -101,6 +101,7 @@ export function cloneElement (n, nodeProps, deep) {
class: cls,
domProps: { ...data.domProps, ...domProps },
scopedSlots: { ...data.scopedSlots, ...scopedSlots },
directives: [...(data.directives || []), ...directives],
})
if (node.componentOptions) {

View File

@ -4,7 +4,7 @@ import Select, { AbstractSelectProps, SelectValue } from '../select'
import Input from '../input'
import InputElement from './InputElement'
import PropTypes from '../_util/vue-types'
import { getComponentFromProp, getOptionProps, filterEmpty } from '../_util/props-util'
import { getComponentFromProp, getOptionProps, filterEmpty, isValidElement, getEvents, getStyle, getClass } from '../_util/props-util'
const DataSourceItemObject = PropTypes.shape({
value: String,
@ -24,7 +24,7 @@ const AutoCompleteProps = {
...AbstractSelectProps,
value: SelectValue,
defaultValue: SelectValue,
dataSource: PropTypes.arrayOf(DataSourceItemType),
dataSource: PropTypes.array,
optionLabelProp: String,
dropdownMatchSelectWidth: PropTypes.bool,
// onChange?: (value: SelectValue) => void;
@ -57,6 +57,13 @@ export default {
const { $slots } = this
const children = filterEmpty($slots.default)
const element = children.length ? children[0] : <Input />
console.log(element)
const eleProps = {
props: getOptionProps(element),
on: getEvents(element),
style: getStyle(element),
class: getClass(element),
}
return (
<InputElement>{element}</InputElement>
)
@ -97,6 +104,9 @@ export default {
options = childArray
} else {
options = dataSource ? dataSource.map((item) => {
if (isValidElement(item)) {
return item
}
switch (typeof item) {
case 'string':
return <Option key={item}>{item}</Option>

View File

@ -7,7 +7,7 @@ import omit from 'omit.js'
import KeyCode from '../_util/KeyCode'
import Input from '../input'
import Icon from '../icon'
import { hasProp, filterEmpty, getOptionProps } from '../_util/props-util'
import { hasProp, filterEmpty, getOptionProps, getStyle, getClass, getAttrs } from '../_util/props-util'
import BaseMixin from '../_util/BaseMixin'
const CascaderOptionType = PropTypes.shape({
@ -83,6 +83,7 @@ function defaultSortFilteredOption (a, b, inputValue) {
const defaultDisplayRender = ({ labels }) => labels.join(' / ')
export default {
inheritAttrs: false,
name: 'ACascader',
mixins: [BaseMixin],
props: CascaderProps,
@ -280,6 +281,7 @@ export default {
[`${prefixCls}-picker-arrow-expand`]: sPopupVisible,
})
const pickerCls = classNames(
getClass(this),
`${prefixCls}-picker`, {
[`${prefixCls}-picker-with-value`]: inputValue,
[`${prefixCls}-picker-disabled`]: disabled,
@ -345,11 +347,13 @@ export default {
keydown: this.handleKeyDown,
change: showSearch ? this.handleInputChange : noop,
},
attrs: getAttrs(this),
}
const children = filterEmpty($slots.default)
const input = children.length ? children : (
<span
class={pickerCls}
style={getStyle(this)}
>
{ showSearch ? <span class={`${prefixCls}-picker-label`}>
{this.getLabel()}

View File

@ -1,6 +1,8 @@
import hasProp from '../_util/props-util'
import classNames from 'classnames'
import hasProp, { getClass, getStyle } from '../_util/props-util'
import PropTypes from '../_util/vue-types'
export default {
inheritAttrs: false,
name: 'ACheckbox',
props: {
prefixCls: {
@ -14,6 +16,7 @@ export default {
value: [String, Number, Boolean],
name: String,
indeterminate: Boolean,
type: PropTypes.string.def('checkbox'),
},
model: {
prop: 'checked',
@ -52,14 +55,13 @@ export default {
handleChange (event) {
const targetChecked = event.target.checked
this.$emit('input', targetChecked)
const { name, value, checked, checkboxGroupContext, sChecked } = this
const { checked, checkboxGroupContext } = this
if ((checked === undefined && !checkboxGroupContext) || (checkboxGroupContext && checkboxGroupContext.sValue === undefined)) {
this.sChecked = targetChecked
}
const target = {
name,
value,
checked: !sChecked,
...this.$props,
checked: targetChecked,
}
this.$emit('change', {
target,
@ -103,9 +105,13 @@ export default {
onChange = () => checkboxGroupContext.toggleOption({ value: props.value })
disabled = props.disabled || checkboxGroupContext.disabled
}
const classString = classNames(getClass(this), {
[`${prefixCls}-wrapper`]: true,
})
return (
<label
class={`${prefixCls}-wrapper`}
class={classString}
style={getStyle(this)}
onMouseenter={this.onMouseEnter}
onMouseleave={this.onMouseLeave}
>

193
components/form/Form.jsx Executable file
View File

@ -0,0 +1,193 @@
import PropTypes from '../_util/vue-types'
import classNames from 'classnames'
import isRegExp from 'lodash/isRegExp'
import createDOMForm from '../vc-form/src/createDOMForm'
import createFormField from '../vc-form/src/createFormField'
import FormItem from './FormItem'
import { FIELD_META_PROP, FIELD_DATA_PROP } from './constants'
import { initDefaultProps } from '../_util/props-util'
export const FormCreateOption = {
onFieldsChange: PropTypes.func,
onValuesChange: PropTypes.func,
mapPropsToFields: PropTypes.func,
withRef: PropTypes.bool,
}
// function create
export const WrappedFormUtils = {
/** 获取一组输入控件的值,如不传入参数,则获取全部组件的值 */
getFieldsValue: PropTypes.func,
/** 获取一个输入控件的值*/
getFieldValue: PropTypes.func,
/** 设置一组输入控件的值*/
setFieldsValue: PropTypes.func,
/** 设置一组输入控件的值*/
setFields: PropTypes.func,
/** 校验并获取一组输入域的值与 Error */
validateFields: PropTypes.func,
// validateFields(fieldNames: Array<string>, options: Object, callback: ValidateCallback): void;
// validateFields(fieldNames: Array<string>, callback: ValidateCallback): void;
// validateFields(options: Object, callback: ValidateCallback): void;
// validateFields(callback: ValidateCallback): void;
// validateFields(): void;
/** 与 `validateFields` 相似,但校验完后,如果校验不通过的菜单域不在可见范围内,则自动滚动进可见范围 */
validateFieldsAndScroll: PropTypes.func,
// validateFieldsAndScroll(fieldNames?: Array<string>, options?: Object, callback?: ValidateCallback): void;
// validateFieldsAndScroll(fieldNames?: Array<string>, callback?: ValidateCallback): void;
// validateFieldsAndScroll(options?: Object, callback?: ValidateCallback): void;
// validateFieldsAndScroll(callback?: ValidateCallback): void;
// validateFieldsAndScroll(): void;
/** 获取某个输入控件的 Error */
getFieldError: PropTypes.func,
getFieldsError: PropTypes.func,
/** 判断一个输入控件是否在校验状态*/
isFieldValidating: PropTypes.func,
isFieldTouched: PropTypes.func,
isFieldsTouched: PropTypes.func,
/** 重置一组输入控件的值与状态,如不传入参数,则重置所有组件 */
resetFields: PropTypes.func,
getFieldDecorator: PropTypes.func,
}
export const FormProps = {
layout: PropTypes.oneOf(['horizontal', 'inline', 'vertical']),
form: PropTypes.shape(WrappedFormUtils).loose,
// onSubmit: React.FormEventHandler<any>;
prefixCls: PropTypes.string,
hideRequiredMark: PropTypes.bool,
}
export const ValidationRule = {
/** validation error message */
message: PropTypes.string,
/** built-in validation type, available options: https://github.com/yiminghe/async-validator#type */
type: PropTypes.string,
/** indicates whether field is required */
required: PropTypes.boolean,
/** treat required fields that only contain whitespace as errors */
whitespace: PropTypes.boolean,
/** validate the exact length of a field */
len: PropTypes.number,
/** validate the min length of a field */
min: PropTypes.number,
/** validate the max length of a field */
max: PropTypes.number,
/** validate the value from a list of possible values */
enum: PropTypes.oneOfType([String, PropTypes.arrayOf(String)]),
/** validate from a regular expression */
pattern: PropTypes.custom(isRegExp),
/** transform a value before validation */
transform: PropTypes.func,
/** custom validate function (Note: callback must be called) */
validator: PropTypes.func,
}
// export type ValidateCallback = (errors: any, values: any) => void;
// export type GetFieldDecoratorOptions = {
// /** Checkbox 'checked' */
// valuePropName?: string;
// /** */
// initialValue?: any;
// /** */
// trigger?: string;
// /** onChange DatePicker (date, dateString) => dateString */
// getValueFromEvent?: (...args: any[]) => any;
// /** */
// validateTrigger?: string | string[];
// /** [async-validator](https://github.com/yiminghe/async-validator) */
// rules?: ValidationRule[];
// /** Radio */
// exclusive?: boolean;
// /** Normalize value to form component */
// normalize?: (value: any, prevValue: any, allValues: any) => any;
// /** Whether stop validate on first rule of error for this field. */
// validateFirst?: boolean;
// };
export default {
name: 'AForm',
props: initDefaultProps(FormProps, {
prefixCls: 'ant-form',
layout: 'horizontal',
hideRequiredMark: false,
}),
// static defaultProps = {
// prefixCls: 'ant-form',
// layout: 'horizontal',
// hideRequiredMark: false,
// onSubmit (e) {
// e.preventDefault()
// },
// };
// static propTypes = {
// prefixCls: PropTypes.string,
// layout: PropTypes.oneOf(['horizontal', 'inline', 'vertical']),
// children: PropTypes.any,
// onSubmit: PropTypes.func,
// hideRequiredMark: PropTypes.bool,
// };
Item: FormItem,
createFormField: createFormField,
create: (options = {}) => {
return createDOMForm({
fieldNameProp: 'id',
...options,
fieldMetaProp: FIELD_META_PROP,
fieldDataProp: FIELD_DATA_PROP,
})
},
// constructor (props) {
// super(props)
// warning(!props.form, 'It is unnecessary to pass `form` to `Form` after antd@1.7.0.')
// }
// shouldComponentUpdate(...args) {
// return PureRenderMixin.shouldComponentUpdate.apply(this, args);
// }
// getChildContext () {
// const { layout } = this.props
// return {
// vertical: layout === 'vertical',
// }
// },
provide () {
return {
FormProps: this.$props,
}
},
methods: {
onSubmit (e) {
const { $listeners } = this
if (!$listeners.submit) {
e.preventDefault()
} else {
this.$emit('submit', e)
}
},
},
render () {
const {
prefixCls, hideRequiredMark, layout, onSubmit, $slots,
} = this
const formClassName = classNames(prefixCls, {
[`${prefixCls}-horizontal`]: layout === 'horizontal',
[`${prefixCls}-vertical`]: layout === 'vertical',
[`${prefixCls}-inline`]: layout === 'inline',
[`${prefixCls}-hide-required-mark`]: hideRequiredMark,
})
return <form onSubmit={onSubmit} class={formClassName}>{$slots.default}</form>
},
}

View File

@ -0,0 +1,339 @@
import PropTypes from '../_util/vue-types'
import classNames from 'classnames'
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, getSlots } from '../_util/props-util'
import getTransitionProps from '../_util/getTransitionProps'
import BaseMixin from '../_util/BaseMixin'
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,
}
export default {
name: 'AFormItem',
__ANT_FORM_ITEM: true,
mixins: [BaseMixin],
props: initDefaultProps(FormItemProps, {
hasFeedback: false,
prefixCls: 'ant-form',
colon: true,
}),
inject: {
FormProps: { default: {}},
},
data () {
return { helpShow: false }
},
mounted () {
warning(
this.getControls(this.$slots.default, true).length <= 1,
'`Form.Item` cannot generate `validateStatus` and `help` automatically, ' +
'while there are more than one `getFieldDecorator` in it.',
)
},
// shouldComponentUpdate(...args: any[]) {
// return PureRenderMixin.shouldComponentUpdate.apply(this, args);
// }
methods: {
getHelpMsg () {
const help = getComponentFromProp(this, 'help')
const onlyControl = this.getOnlyControl()
if (help === undefined && onlyControl) {
const errors = this.getField().errors
return errors ? errors.map((e) => e.message).join(', ') : ''
}
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 attrs = child.data && child.data.attrs
if (!attrs) {
continue
}
const slots = getSlots(child)
if (FIELD_META_PROP in attrs) { // And means FIELD_DATA_PROP in chidl.props, too.
controls.push(child)
} else if (slots.default) {
controls = controls.concat(this.getControls(slots.default, recursively))
}
}
return controls
},
getOnlyControl () {
const child = this.getControls(this.$slots.default, 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 () {
this.setState({ helpShow: false })
},
renderHelp () {
const prefixCls = this.prefixCls
const help = this.getHelpMsg()
const children = help ? (
<div class={`${prefixCls}-explain`} key='help'>
{help}
</div>
) : null
const transitionProps = getTransitionProps('show-help', {
afterLeave: this.onHelpAnimEnd,
})
if (children) {
this.setState({ helpShow: true })
}
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',
})
}
return (
<div class={classes}>
<span class={`${props.prefixCls}-item-children`}>{c1}</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 () {
const { $slots } = this
return [
this.renderLabel(),
this.renderWrapper(
this.renderValidateWrapper(
filterEmpty($slots.default || []),
this.renderHelp(),
this.renderExtra(),
),
),
]
},
renderFormItem (children) {
const props = this.$props
const prefixCls = props.prefixCls
const itemClassName = {
[`${prefixCls}-item`]: true,
[`${prefixCls}-item-with-help`]: !!this.getHelpMsg() || this.helpShow,
[`${prefixCls}-item-no-colon`]: !props.colon,
}
return (
<Row class={classNames(itemClassName)}>
{children}
</Row>
)
},
},
render () {
const children = this.renderChildren()
return this.renderFormItem(children)
},
}

View File

@ -0,0 +1,2 @@
export const FIELD_META_PROP = 'data-__meta'
export const FIELD_DATA_PROP = 'data-__field'

View File

@ -0,0 +1,134 @@
<cn>
#### 高级搜索
三列栅格式的表单排列方式常用于数据表格的高级搜索
有部分定制的样式代码由于输入标签长度不确定需要根据具体情况自行调整
</cn>
<us>
#### Advanced search
Three columns layout is often used for advanced searching of data table.
Because the width of label is not fixed, you may need to adjust it by customizing its style.
</us>
<script>
import { Form } from 'vue-antd-ui'
const AdvancedSearchForm = {
data () {
return {
expand: false,
}
},
methods: {
handleSearch (e) {
e.preventDefault()
this.form.validateFields((error, values) => {
console.log('error', error)
console.log('Received values of form: ', values)
})
},
handleReset () {
this.form.resetFields()
},
toggle () {
this.expand = !this.expand
},
// To generate mock Form.Item
getFields () {
const count = this.expand ? 10 : 6
const { getFieldDecorator } = this.form
const children = []
for (let i = 0; i < 10; i++) {
children.push(
<a-col span={8} key={i} style={{ display: i < count ? 'block' : 'none' }}>
<a-form-item label={`Field ${i}`}>
{getFieldDecorator(`field-${i}`, {
rules: [{
required: true,
message: 'Input something!',
}],
})(
<a-input placeholder='placeholder' />
)}
</a-form-item>
</a-col>
)
}
return children
},
},
render () {
return (
<a-form
class='ant-advanced-search-form'
onSubmit={this.handleSearch}
>
<a-row gutter={24}>{this.getFields()}</a-row>
<a-row>
<a-col span={24} style={{ textAlign: 'right' }}>
<a-button type='primary' htmlType='submit'>Search</a-button>
<a-button style={{ marginLeft: '8px' }} onClick={this.handleReset}>
Clear
</a-button>
<a style={{ marginLeft: '8px', fontSize: '12px' }} onClick={this.toggle}>
Collapse <a-icon type={this.expand ? 'up' : 'down'} />
</a>
</a-col>
</a-row>
</a-form>
)
},
}
const WrappedAdvancedSearchForm = Form.create()(AdvancedSearchForm)
export default {
render () {
return (
<div id='components-form-demo-advanced-search'>
<WrappedAdvancedSearchForm />
<div class='search-result-list'>Search Result List</div>
</div>
)
},
}
</script>
<style>
.ant-advanced-search-form {
padding: 24px;
background: #fbfbfb;
border: 1px solid #d9d9d9;
border-radius: 6px;
}
.ant-advanced-search-form .ant-form-item {
display: flex;
}
.ant-advanced-search-form .ant-form-item-control-wrapper {
flex: 1;
}
#components-form-demo-advanced-search .ant-form {
max-width: none;
}
#components-form-demo-advanced-search .search-result-list {
margin-top: 16px;
border: 1px dashed #e9e9e9;
border-radius: 6px;
background-color: #fafafa;
min-height: 200px;
text-align: center;
padding-top: 80px;
}
</style>

View File

@ -0,0 +1,82 @@
<cn>
#### 表单联动
使用 `setFieldsValue` 来动态设置其他控件的值
</cn>
<us>
#### Coordinated Controls
Use `setFieldsValue` to set other control's value programmaticly.
</us>
<script>
import { Form } from 'vue-antd-ui'
const CoordinatedForm = {
methods: {
handleSubmit (e) {
e.preventDefault()
this.form.validateFields((err, values) => {
if (!err) {
console.log('Received values of form: ', values)
}
})
},
handleSelectChange (value) {
console.log(value)
this.form.setFieldsValue({
note: `Hi, ${value === 'male' ? 'man' : 'lady'}!`,
})
},
},
render () {
const { getFieldDecorator } = this.form
return (
<a-form onSubmit={this.handleSubmit}>
<a-form-item
label='Note'
labelCol={{ span: 5 }}
wrapperCol={{ span: 12 }}
>
{getFieldDecorator('note', {
rules: [{ required: true, message: 'Please input your note!' }],
})(
<a-input />
)}
</a-form-item>
<a-form-item
label='Gender'
labelCol={{ span: 5 }}
wrapperCol={{ span: 12 }}
>
{getFieldDecorator('gender', {
rules: [{ required: true, message: 'Please select your gender!' }],
})(
<a-select
placeholder='Select a option and change input text above'
onChange={this.handleSelectChange}
>
<a-select-option value='male'>male</a-select-option>
<a-select-option value='female'>female</a-select-option>
</a-select>
)}
</a-form-item>
<a-form-item
wrapperCol={{ span: 12, offset: 5 }}
>
<a-button type='primary' htmlType='submit'>
Submit
</a-button>
</a-form-item>
</a-form>
)
},
}
export default Form.create()(CoordinatedForm)
</script>

View File

@ -0,0 +1,129 @@
<cn>
#### 自定义表单控件
自定义或第三方的表单控件也可以与 Form 组件一起使用只要该组件遵循以下的约定
> * 提供受控属性 `value` 或其它与 [`valuePropName`](/ant-design/components/form-cn/#getFieldDecorator(id,-options)-)
> * 提供 `onChange` 事件或 [`trigger`](/ant-design/components/form-cn/#getFieldDecorator(id,-options)-)
> * 不能是函数式组件
</cn>
<us>
#### Customized Form Controls
Customized or third-party form controls can be used in Form, too. Controls must follow these conventions:
> * It has a controlled property `value` or other name which is equal to the value of [`valuePropName`](/ant-design/components/form/#getFieldDecorator(id,-options)-parameters).
> * It has event `onChange` or an event which name is equal to the value of [`trigger`](/ant-design/components/form/#getFieldDecorator(id,-options)-parameters).
> * It must be a class component.
</us>
<script>
import { Form } from 'vue-antd-ui'
const hasProp = (instance, prop) => {
const $options = instance.$options || {}
const propsData = $options.propsData || {}
return prop in propsData
}
const PriceInput = {
props: ['value'],
data () {
const value = this.value || {}
return {
number: value.number || 0,
currency: value.currency || 'rmb',
}
},
watch: {
value (val = {}) {
this.number = val.number || 0
this.currency = val.currency || 'rmb'
},
},
methods: {
handleNumberChange (e) {
const number = parseInt(e.target.value || 0, 10)
if (isNaN(number)) {
return
}
if (!hasProp(this, 'value')) {
this.number = number
}
this.triggerChange({ number })
},
handleCurrencyChange (currency) {
if (!hasProp(this, 'value')) {
this.currency = currency
}
this.triggerChange({ currency })
},
triggerChange (changedValue) {
// Should provide an event to pass value to Form.
this.$emit('change', Object.assign({}, this.$data, changedValue))
},
},
render () {
const { number, currency } = this
return (
<span>
<a-input
type='text'
value={number}
onChange={this.handleNumberChange}
style={{ width: '65%', marginRight: '3%' }}
/>
<a-select
value={currency}
style={{ width: '32%' }}
onChange={this.handleCurrencyChange}
>
<a-select-option value='rmb'>RMB</a-select-option>
<a-select-option value='dollar'>Dollar</a-select-option>
</a-select>
</span>
)
},
}
const Demo = {
methods: {
handleSubmit (e) {
e.preventDefault()
this.form.validateFields((err, values) => {
if (!err) {
console.log('Received values of form: ', values)
}
})
},
checkPrice (rule, value, callback) {
if (value.number > 0) {
callback()
return
}
callback('Price must greater than zero!')
},
},
render () {
const { getFieldDecorator } = this.form
return (
<a-form layout='inline' onSubmit={this.handleSubmit}>
<a-form-item label='Price'>
{getFieldDecorator('price', {
initialValue: { number: 0, currency: 'rmb' },
rules: [{ validator: this.checkPrice }],
})(<PriceInput />)}
</a-form-item>
<a-form-item>
<a-button type='primary' htmlType='submit'>Submit</a-button>
</a-form-item>
</a-form>
)
},
}
export default Form.create()(Demo)
</script>

View File

@ -0,0 +1,144 @@
<cn>
#### 动态增减表单项
动态增加减少表单项
</cn>
<us>
#### Dynamic Form Item
Add or remove form items dynamically.
</us>
<script>
import { Form } from 'vue-antd-ui'
let uuid = 0
const DynamicFieldSet = {
methods: {
remove (k) {
const { form } = this
// can use data-binding to get
const keys = form.getFieldValue('keys')
// We need at least one passenger
if (keys.length === 1) {
return
}
// can use data-binding to set
form.setFieldsValue({
keys: keys.filter(key => key !== k),
})
},
add () {
const { form } = this
// can use data-binding to get
const keys = form.getFieldValue('keys')
const nextKeys = keys.concat(uuid)
uuid++
// can use data-binding to set
// important! notify form to detect changes
form.setFieldsValue({
keys: nextKeys,
})
},
handleSubmit (e) {
e.preventDefault()
this.form.validateFields((err, values) => {
if (!err) {
console.log('Received values of form: ', values)
}
})
},
},
render () {
const { getFieldDecorator, getFieldValue } = this.form
const formItemLayout = {
labelCol: {
xs: { span: 24 },
sm: { span: 4 },
},
wrapperCol: {
xs: { span: 24 },
sm: { span: 20 },
},
}
const formItemLayoutWithOutLabel = {
wrapperCol: {
xs: { span: 24, offset: 0 },
sm: { span: 20, offset: 4 },
},
}
getFieldDecorator('keys', { initialValue: [] })
const keys = getFieldValue('keys')
const formItems = keys.map((k, index) => {
return (
<a-form-item
{...{ props: (index === 0 ? formItemLayout : formItemLayoutWithOutLabel) }}
label={index === 0 ? 'Passengers' : ''}
required={false}
key={k}
>
{getFieldDecorator(`names[${k}]`, {
validateTrigger: ['onChange', 'onBlur'],
rules: [{
required: true,
whitespace: true,
message: "Please input passenger's name or delete this field.",
}],
})(
<a-input placeholder='passenger name' style={{ width: '60%', marginRight: '8px' }} />
)}
{keys.length > 1 ? (
<a-icon
class='dynamic-delete-button'
type='minus-circle-o'
disabled={keys.length === 1}
onClick={() => this.remove(k)}
/>
) : null}
</a-form-item>
)
})
return (
<a-form onSubmit={this.handleSubmit}>
{formItems}
<a-form-item {...{ props: formItemLayoutWithOutLabel }}>
<a-button type='dashed' onClick={this.add} style={{ width: '60%' }}>
<a-icon type='plus' /> Add field
</a-button>
</a-form-item>
<a-form-item {...{ props: formItemLayoutWithOutLabel }}>
<a-button type='primary' htmlType='submit'>Submit</a-button>
</a-form-item>
</a-form>
)
},
}
export default Form.create()(DynamicFieldSet)
</script>
<style>
.dynamic-delete-button {
cursor: pointer;
position: relative;
top: 4px;
font-size: 24px;
color: #999;
transition: all .3s;
}
.dynamic-delete-button:hover {
color: #777;
}
.dynamic-delete-button[disabled] {
cursor: not-allowed;
opacity: 0.5;
}
</style>

View File

@ -0,0 +1,94 @@
<cn>
#### 动态校验规则
根据不同情况执行不同的校验规则
</cn>
<us>
#### Dynamic Rules
Perform different check rules according to different situations.
</us>
<script>
import { Form } from 'vue-antd-ui'
const formItemLayout = {
labelCol: { span: 4 },
wrapperCol: { span: 8 },
}
const formTailLayout = {
labelCol: { span: 4 },
wrapperCol: { span: 8, offset: 4 },
}
const DynamicRule = {
data () {
return {
checkNick: false,
}
},
methods: {
check () {
this.form.validateFields(
(err) => {
if (!err) {
console.info('success')
}
},
)
},
handleChange (e) {
this.checkNick = e.target.checked
this.$nextTick(() => {
this.form.validateFields(['nickname'], { force: true })
})
},
},
render () {
const { getFieldDecorator } = this.form
return (
<div>
<a-form-item {...{ props: formItemLayout }} label='Name'>
{getFieldDecorator('username', {
rules: [{
required: true,
message: 'Please input your name',
}],
})(
<a-input placeholder='Please input your name' />
)}
</a-form-item>
<a-form-item {...{ props: formItemLayout }} label='Nickname'>
{getFieldDecorator('nickname', {
rules: [{
required: this.checkNick,
message: 'Please input your nickname',
}],
})(
<a-input placeholder='Please input your nickname' />
)}
</a-form-item>
<a-form-item {...{ props: formTailLayout }}>
<a-checkbox
value={this.checkNick}
onChange={this.handleChange}
>
Nickname is required
</a-checkbox>
</a-form-item>
<a-form-item {...{ props: formTailLayout }}>
<a-button type='primary' onClick={this.check}>
Check
</a-button>
</a-form-item>
</div>
)
},
}
export default Form.create()(DynamicRule)
</script>

View File

@ -0,0 +1,105 @@
<cn>
#### 弹出层中的新建表单
当用户访问一个展示了某个列表的页面想新建一项但又不想跳转页面时可以用 Modal 弹出一个表单用户填写必要信息后创建新的项
</cn>
<us>
#### Form in Modal to Create
When user visit a page with a list of items, and want to create a new item. The page can popup a form in Modal, then let user fill in the form to create an item.
</us>
<script>
import { Form } from 'vue-antd-ui'
const CollectionCreateForm = Form.create()(
{
props: ['visible'],
render () {
const { visible, form } = this
const { getFieldDecorator } = form
return (
<a-modal
visible={visible}
title='Create a new collection'
okText='Create'
onCancel={() => { this.$emit('cancel') }}
onOk={() => { this.$emit('create') }}
>
<a-form layout='vertical'>
<a-form-item label='Title'>
{getFieldDecorator('title', {
rules: [{ required: true, message: 'Please input the title of collection!' }],
})(
<a-input />
)}
</a-form-item>
<a-form-item label='Description'>
{getFieldDecorator('description')(<a-input type='textarea' />)}
</a-form-item>
<a-form-item className='collection-create-form_last-form-item'>
{getFieldDecorator('modifier', {
initialValue: 'public',
})(
<a-radio-group>
<a-radio value='public'>Public</a-radio>
<a-radio value='private'>Private</a-radio>
</a-radio-group>
)}
</a-form-item>
</a-form>
</a-modal>
)
},
}
)
export default {
data () {
return {
visible: false,
}
},
methods: {
showModal () {
this.visible = true
},
handleCancel () {
this.visible = false
},
handleCreate () {
const form = this.formRef.form
form.validateFields((err, values) => {
if (err) {
return
}
console.log('Received values of form: ', values)
form.resetFields()
this.visible = false
})
},
saveFormRef (formRef) {
this.formRef = formRef
},
},
render () {
return (
<div>
<a-button type='primary' onClick={this.showModal}>New Collection</a-button>
<CollectionCreateForm
wrappedComponentRef={this.saveFormRef}
visible={this.visible}
onCancel={this.handleCancel}
onCreate={this.handleCreate}
/>
</div>
)
},
}
</script>

View File

@ -0,0 +1,92 @@
<cn>
#### 表单数据存储于上层组件
通过使用 `onFieldsChange` `mapPropsToFields`可以把表单的数据存储到上层组件
**注意**
`mapPropsToFields` 里面返回的表单域数据必须使用 `Form.createFormField` 包装
上层组件传递的属性必须在`Form.create({ props: ...})`的props中声明
</cn>
<us>
#### Store Form Data into Upper Component
We can store form data into upper component.
**Note:**
You must wrap field data with `Form.createFormField` in `mapPropsToFields`.
The properties passed by the upper component must be declared in the props of `Form.create({ props: ...})`.
</us>
<script>
import { Form } from 'vue-antd-ui'
const CustomizedForm = Form.create({
props: ['username'], // must declare like vue `props` https://vuejs.org/v2/api/#props
onFieldsChange (instance, changedFields) {
instance.$emit('change', changedFields)
},
mapPropsToFields (props) {
return {
username: Form.createFormField({
...props.username,
value: props.username.value,
}),
}
},
onValuesChange (_, values) {
console.log(values)
},
})({
render () {
const { getFieldDecorator } = this.form
return (
<a-form layout='inline'>
<a-form-item label='Username'>
{getFieldDecorator('username', {
rules: [{ required: true, message: 'Username is required!' }],
})(<a-input />)}
</a-form-item>
</a-form>
)
},
})
export default {
data () {
return {
fields: {
username: {
value: 'benjycui',
},
},
}
},
methods: {
handleFormChange (changedFields) {
this.fields = { ...this.fields, ...changedFields }
},
},
render () {
const fields = this.fields
return (
<div id='components-form-demo-global-state'>
<CustomizedForm {...{ props: fields }} onChange={this.handleFormChange} />
<pre class='language-bash'>
{JSON.stringify(fields, null, 2)}
</pre>
</div>
)
},
}
</script>
<style>
#components-form-demo-global-state .language-bash {
max-width: 400px;
border-radius: 6px;
margin-top: 24px;
}
</style>

View File

@ -0,0 +1,82 @@
<cn>
#### 水平登录栏
水平登录栏常用在顶部导航栏中
</cn>
<us>
#### Horizontal Login Form
Horizontal login form is often used in navigation bar.
</us>
<script>
import { Form } from 'vue-antd-ui'
function hasErrors (fieldsError) {
return Object.keys(fieldsError).some(field => fieldsError[field])
}
const HorizontalLoginForm = {
mounted () {
// To disabled submit button at the beginning.
this.form.validateFields()
},
methods: {
handleSubmit (e) {
e.preventDefault()
this.form.validateFields((err, values) => {
if (!err) {
console.log('Received values of form: ', values)
}
})
},
},
render () {
const { getFieldDecorator, getFieldsError, getFieldError, isFieldTouched } = this.form
// Only show error after a field is touched.
const userNameError = isFieldTouched('userName') && getFieldError('userName')
const passwordError = isFieldTouched('password') && getFieldError('password')
return (
<a-form layout='inline' onSubmit={this.handleSubmit}>
<a-form-item
validateStatus={userNameError ? 'error' : ''}
help={userNameError || ''}
>
{getFieldDecorator('userName', {
rules: [{ required: true, message: 'Please input your username!' }],
})(
<a-input prefix={<a-icon type='user' style={{ color: 'rgba(0,0,0,.25)' }} />} placeholder='Username' />
)}
</a-form-item>
<a-form-item
validateStatus={passwordError ? 'error' : ''}
help={passwordError || ''}
>
{getFieldDecorator('password', {
rules: [{ required: true, message: 'Please input your Password!' }],
})(
<a-input prefix={<a-icon type='lock' style={{ color: 'rgba(0,0,0,.25)' }} />} type='password' placeholder='Password' />
)}
</a-form-item>
<a-form-item>
<a-button
type='primary'
htmlType='submit'
disabled={hasErrors(getFieldsError())}
>
Log in
</a-button>
</a-form-item>
</a-form>
)
},
}
export default Form.create()(HorizontalLoginForm)
</script>

View File

@ -0,0 +1,146 @@
<script>
import AdvancedSearch from './advanced-search'
import AdvancedSearchString from '!raw-loader!./advanced-search'
import Coordinated from './coordinated'
import CoordinatedString from '!raw-loader!./coordinated'
import CustomizedFormControls from './customized-form-controls'
import CustomizedFormControlsString from '!raw-loader!./customized-form-controls'
import DynamicFormItem from './dynamic-form-item'
import DynamicFormItemString from '!raw-loader!./dynamic-form-item'
import DynamicRule from './dynamic-rule'
import DynamicRuleString from '!raw-loader!./dynamic-rule'
import FormInModal from './form-in-modal'
import FormInModalString from '!raw-loader!./form-in-modal'
import GlobalState from './global-state'
import GlobalStateString from '!raw-loader!./global-state'
import HorizontalLogin from './horizontal-login'
import HorizontalLoginString from '!raw-loader!./horizontal-login'
import Layout from './layout'
import LayoutString from '!raw-loader!./layout'
import NormalLogin from './normal-login'
import NormalLoginString from '!raw-loader!./normal-login'
import Register from './register'
import RegisterString from '!raw-loader!./register'
import TimeRelatedControls from './time-related-controls'
import TimeRelatedControlsString from '!raw-loader!./time-related-controls'
import ValidateOther from './validate-other'
import ValidateOtherString from '!raw-loader!./validate-other'
import ValidateStatic from './validate-static'
import ValidateStaticString from '!raw-loader!./validate-static'
import WithoutFormCreate from './without-form-create'
import WithoutFormCreateString from '!raw-loader!./without-form-create'
import CN from '../index.zh-CN'
import US from '../index.en-US'
const md = {
cn: `# Form 表单
具有数据收集校验和提交功能的表单包含复选框单选框输入框下拉选择框等元素
## 表单
我们为 \`form\` 提供了以下三种排列方式:
- 水平排列标签和表单控件水平排列默认
- 垂直排列标签和表单控件上下垂直排列
- 行内排列表单项水平行内排列
## 表单域
表单一定会包含表单域表单域可以是输入控件标准表单域标签下拉菜单文本域等
这里我们封装了表单域 \`<Form.Item />\`
**注意** 如果使用 \`Form.create\` 处理表单使其具有自动收集数据并校验的功能,建议使用\`jsx\`
## 代码演示
`,
us: `# Form
Form is used to collect, validate, and submit the user input, usually contains various form items including checkbox, radio, input, select, and etc.
## Form
You can align the controls of a \`form\` using the \`layout\` prop
- \`horizontal\`to horizontally align the \`label\`s and controls of the fields. (Default)
- \`vertical\`to vertically align the \`label\`s and controls of the fields.
- \`inline\`to render form fields in one line.
## Form fields
A form consists of one or more form fields whose type includes input, textarea, checkbox, radio, select, tag, and more.
A form field is defined using \`<Form.Item />\`.
**Note:** If you use \`Form.create\` to process forms with automatic data collection and validation, we recommend using \`jsx\`
## Examples
`,
}
export default {
category: 'Components',
subtitle: '表单',
type: 'Data Entry',
cols: 1,
title: 'Form',
render () {
return (
<div>
<md cn={md.cn} us={md.us} />
<demo-container code={AdvancedSearchString}>
<AdvancedSearch />
</demo-container>
<demo-container code={CoordinatedString}>
<Coordinated />
</demo-container>
<demo-container code={CustomizedFormControlsString}>
<CustomizedFormControls />
</demo-container>
<demo-container code={DynamicFormItemString}>
<DynamicFormItem />
</demo-container>
<demo-container code={DynamicRuleString}>
<DynamicRule />
</demo-container>
<demo-container code={FormInModalString}>
<FormInModal />
</demo-container>
<demo-container code={GlobalStateString}>
<GlobalState />
</demo-container>
<demo-container code={HorizontalLoginString}>
<HorizontalLogin />
</demo-container>
<demo-container code={LayoutString}>
<Layout />
</demo-container>
<demo-container code={NormalLoginString}>
<NormalLogin />
</demo-container>
<demo-container code={RegisterString}>
<Register />
</demo-container>
<demo-container code={TimeRelatedControlsString}>
<TimeRelatedControls />
</demo-container>
<demo-container code={ValidateStaticString}>
<ValidateStatic />
</demo-container>
<demo-container code={WithoutFormCreateString}>
<WithoutFormCreate />
</demo-container>
<demo-container code={ValidateOtherString}>
<ValidateOther />
</demo-container>
<api>
<CN slot='cn' />
<US />
</api>
</div>
)
},
}
</script>
<style>
.code-box-demo .ant-form:not(.ant-form-inline):not(.ant-form-vertical) {
max-width: 600px;
}
</style>

View File

@ -0,0 +1,81 @@
<cn>
#### 表单布局
表单有三种布局
</cn>
<us>
#### Form Layout
There are three layout for form: `horizontal`, `vertical`, `inline`.
</us>
<template>
<div>
<a-form :layout="formLayout">
<a-form-item
label='Form Layout'
:labelCol="formItemLayout.labelCol"
:wrapperCol="formItemLayout.wrapperCol"
>
<a-radio-group defaultValue='horizontal' @change="handleFormLayoutChange">
<a-radio-button value='horizontal'>Horizontal</a-radio-button>
<a-radio-button value='vertical'>Vertical</a-radio-button>
<a-radio-button value='inline'>Inline</a-radio-button>
</a-radio-group>
</a-form-item>
<a-form-item
label='Field A'
:labelCol="formItemLayout.labelCol"
:wrapperCol="formItemLayout.wrapperCol"
>
<a-input placeholder='input placeholder' />
</a-form-item>
<a-form-item
label='Field B'
:labelCol="formItemLayout.labelCol"
:wrapperCol="formItemLayout.wrapperCol"
>
<a-input placeholder='input placeholder' />
</a-form-item>
<a-form-item
:wrapperCol="buttonItemLayout.wrapperCol"
>
<a-button type='primary'>Submit</a-button>
</a-form-item>
</a-form>
</div>
</template>
<script>
export default {
data () {
return {
formLayout: 'horizontal',
}
},
methods: {
handleFormLayoutChange (e) {
this.formLayout = e.target.value
},
},
computed: {
formItemLayout () {
const { formLayout } = this
return formLayout === 'horizontal' ? {
labelCol: { span: 4 },
wrapperCol: { span: 14 },
} : {}
},
buttonItemLayout () {
const { formLayout } = this
return formLayout === 'horizontal' ? {
wrapperCol: { span: 14, offset: 4 },
} : {}
},
},
}
</script>

View File

@ -0,0 +1,79 @@
<cn>
#### 登录框
普通的登录框可以容纳更多的元素
</cn>
<us>
#### Login Form
Normal login form which can contain more elements.
</us>
<script>
import { Form } from 'vue-antd-ui'
const NormalLoginForm = {
methods: {
handleSubmit (e) {
e.preventDefault()
this.form.validateFields((err, values) => {
if (!err) {
console.log('Received values of form: ', values)
}
})
},
},
render () {
const { getFieldDecorator } = this.form
return (
<a-form id='components-form-demo-normal-login' onSubmit={this.handleSubmit} class='login-form'>
<a-form-item>
{getFieldDecorator('userName', {
rules: [{ required: true, message: 'Please input your username!' }],
})(
<a-input prefix={<a-icon type='user' style={{ color: 'rgba(0,0,0,.25)' }} />} placeholder='Username' />
)}
</a-form-item>
<a-form-item>
{getFieldDecorator('password', {
rules: [{ required: true, message: 'Please input your Password!' }],
})(
<a-input prefix={<a-icon type='lock' style={{ color: 'rgba(0,0,0,.25)' }} />} type='password' placeholder='Password' />
)}
</a-form-item>
<a-form-item>
{getFieldDecorator('remember', {
valuePropName: 'checked',
initialValue: true,
})(
<a-checkbox>Remember me</a-checkbox>
)}
<a class='login-form-forgot' href=''>Forgot password</a>
<a-button type='primary' htmlType='submit' class='login-form-button'>
Log in
</a-button>
Or <a href=''>register now!</a>
</a-form-item>
</a-form>
)
},
}
export default Form.create()(NormalLoginForm)
</script>
<style>
#components-form-demo-normal-login .login-form {
max-width: 300px;
}
#components-form-demo-normal-login .login-form-forgot {
float: right;
}
#components-form-demo-normal-login .login-form-button {
width: 100%;
}
</style>

View File

@ -0,0 +1,260 @@
<cn>
#### 注册新用户
用户填写必须的信息以注册新用户
</cn>
<us>
#### Registration
Fill in this form to create a new account for you.
</us>
<script>
import { Form } from 'vue-antd-ui'
const residences = [{
value: 'zhejiang',
label: 'Zhejiang',
children: [{
value: 'hangzhou',
label: 'Hangzhou',
children: [{
value: 'xihu',
label: 'West Lake',
}],
}],
}, {
value: 'jiangsu',
label: 'Jiangsu',
children: [{
value: 'nanjing',
label: 'Nanjing',
children: [{
value: 'zhonghuamen',
label: 'Zhong Hua Men',
}],
}],
}]
const RegistrationForm = {
data () {
return {
confirmDirty: false,
autoCompleteResult: [],
}
},
methods: {
handleSubmit (e) {
e.preventDefault()
this.form.validateFieldsAndScroll((err, values) => {
if (!err) {
console.log('Received values of form: ', values)
}
})
},
handleConfirmBlur (e) {
const value = e.target.value
this.confirmDirty = this.confirmDirty || !!value
},
compareToFirstPassword (rule, value, callback) {
const form = this.form
if (value && value !== form.getFieldValue('password')) {
callback('Two passwords that you enter is inconsistent!')
} else {
callback()
}
},
validateToNextPassword (rule, value, callback) {
const form = this.form
if (value && this.confirmDirty) {
form.validateFields(['confirm'], { force: true })
}
callback()
},
handleWebsiteChange (value) {
let autoCompleteResult
if (!value) {
autoCompleteResult = []
} else {
autoCompleteResult = ['.com', '.org', '.net'].map(domain => `${value}${domain}`)
}
this.autoCompleteResult = autoCompleteResult
},
},
render () {
const { getFieldDecorator } = this.form
const { autoCompleteResult } = this
const formItemLayout = {
labelCol: {
xs: { span: 24 },
sm: { span: 8 },
},
wrapperCol: {
xs: { span: 24 },
sm: { span: 16 },
},
}
const tailFormItemLayout = {
wrapperCol: {
xs: {
span: 24,
offset: 0,
},
sm: {
span: 16,
offset: 8,
},
},
}
const prefixSelector = getFieldDecorator('prefix', {
initialValue: '86',
})(
<a-select style={{ width: '70px' }}>
<a-select-option value='86'>+86</a-select-option>
<a-select-option value='87'>+87</a-select-option>
</a-select>
)
const websiteOptions = autoCompleteResult.map(website => (
<a-select-option key={website}>{website}</a-select-option>
))
return (
<a-form onSubmit={this.handleSubmit}>
<a-form-item
{...{ props: formItemLayout }}
label='E-mail'
>
{getFieldDecorator('email', {
rules: [{
type: 'email', message: 'The input is not valid E-mail!',
}, {
required: true, message: 'Please input your E-mail!',
}],
})(
<a-input />
)}
</a-form-item>
<a-form-item
{...{ props: formItemLayout }}
label='Password'
>
{getFieldDecorator('password', {
rules: [{
required: true, message: 'Please input your password!',
}, {
validator: this.validateToNextPassword,
}],
})(
<a-input type='password' />
)}
</a-form-item>
<a-form-item
{...{ props: formItemLayout }}
label='Confirm Password'
>
{getFieldDecorator('confirm', {
rules: [{
required: true, message: 'Please confirm your password!',
}, {
validator: this.compareToFirstPassword,
}],
})(
<a-input type='password' onBlur={this.handleConfirmBlur} />
)}
</a-form-item>
<a-form-item
{...{ props: formItemLayout }}
label={(
<span>
Nickname&nbsp;
<a-tooltip title='What do you want others to call you?'>
<a-icon type='question-circle-o' />
</a-tooltip>
</span>
)}
>
{getFieldDecorator('nickname', {
rules: [{ required: true, message: 'Please input your nickname!', whitespace: true }],
})(
<a-input />
)}
</a-form-item>
<a-form-item
{...{ props: formItemLayout }}
label='Habitual Residence'
>
{getFieldDecorator('residence', {
initialValue: ['zhejiang', 'hangzhou', 'xihu'],
rules: [{ type: 'array', required: true, message: 'Please select your habitual residence!' }],
})(
<a-cascader options={residences} />
)}
</a-form-item>
<a-form-item
{...{ props: formItemLayout }}
label='Phone Number'
>
{getFieldDecorator('phone', {
rules: [{ required: true, message: 'Please input your phone number!' }],
})(
<a-input addonBefore={prefixSelector} style={{ width: '100%' }} />
)}
</a-form-item>
<a-form-item
{...{ props: formItemLayout }}
label='Website'
>
{getFieldDecorator('website', {
rules: [{ required: true, message: 'Please input website!' }],
})(
<a-auto-complete
dataSource={websiteOptions}
onChange={this.handleWebsiteChange}
placeholder='website'
>
<a-input />
</a-auto-complete>
)}
</a-form-item>
<a-form-item
{...{ props: formItemLayout }}
label='Captcha'
extra='We must make sure that your are a human.'
>
<a-row gutter={8}>
<a-col span={12}>
{getFieldDecorator('captcha', {
rules: [{ required: true, message: 'Please input the captcha you got!' }],
})(
<a-input />
)}
</a-col>
<a-col span={12}>
<a-button>Get captcha</a-button>
</a-col>
</a-row>
</a-form-item>
<a-form-item {...{ props: tailFormItemLayout }}>
{getFieldDecorator('agreement', {
valuePropName: 'checked',
})(
<a-checkbox>I have read the <a href=''>agreement</a></a-checkbox>
)}
</a-form-item>
<a-form-item {...{ props: tailFormItemLayout }}>
<a-button type='primary' htmlType='submit'>Register</a-button>
</a-form-item>
</a-form>
)
},
}
export default Form.create()(RegistrationForm)
</script>

View File

@ -0,0 +1,131 @@
<cn>
#### 时间类控件
时间类组件的 `value` `moment` 类型所以在提交前需要预处理
</cn>
<us>
#### Time-related Controls
the `value` of time-related components is `moment`. So, we need to pre-process those values.
</us>
<script>
import { Form } from 'vue-antd-ui'
const TimeRelatedForm = {
methods: {
handleSubmit (e) {
e.preventDefault()
this.form.validateFields((err, fieldsValue) => {
if (err) {
return
}
// Should format date value before submit.
const rangeValue = fieldsValue['range-picker']
const rangeTimeValue = fieldsValue['range-time-picker']
const values = {
...fieldsValue,
'date-picker': fieldsValue['date-picker'].format('YYYY-MM-DD'),
'date-time-picker': fieldsValue['date-time-picker'].format('YYYY-MM-DD HH:mm:ss'),
'month-picker': fieldsValue['month-picker'].format('YYYY-MM'),
'range-picker': [rangeValue[0].format('YYYY-MM-DD'), rangeValue[1].format('YYYY-MM-DD')],
'range-time-picker': [
rangeTimeValue[0].format('YYYY-MM-DD HH:mm:ss'),
rangeTimeValue[1].format('YYYY-MM-DD HH:mm:ss'),
],
'time-picker': fieldsValue['time-picker'].format('HH:mm:ss'),
}
console.log('Received values of form: ', values)
})
},
},
render () {
const { getFieldDecorator } = this.form
const formItemLayout = {
labelCol: {
xs: { span: 24 },
sm: { span: 8 },
},
wrapperCol: {
xs: { span: 24 },
sm: { span: 16 },
},
}
const config = {
rules: [{ type: 'object', required: true, message: 'Please select time!' }],
}
const rangeConfig = {
rules: [{ type: 'array', required: true, message: 'Please select time!' }],
}
return (
<a-form onSubmit={this.handleSubmit}>
<a-form-item
{...{ props: formItemLayout }}
label='DatePicker'
>
{getFieldDecorator('date-picker', config)(
<a-date-picker />
)}
</a-form-item>
<a-form-item
{...{ props: formItemLayout }}
label='DatePicker[showTime]'
>
{getFieldDecorator('date-time-picker', config)(
<a-date-picker showTime format='YYYY-MM-DD HH:mm:ss' />
)}
</a-form-item>
<a-form-item
{...{ props: formItemLayout }}
label='MonthPicker'
>
{getFieldDecorator('month-picker', config)(
<a-monthPicker />
)}
</a-form-item>
<a-form-item
{...{ props: formItemLayout }}
label='RangePicker'
>
{getFieldDecorator('range-picker', rangeConfig)(
<a-range-picker />
)}
</a-form-item>
<a-form-item
{...{ props: formItemLayout }}
label='RangePicker[showTime]'
>
{getFieldDecorator('range-time-picker', rangeConfig)(
<a-range-picker showTime format='YYYY-MM-DD HH:mm:ss' />
)}
</a-form-item>
<a-form-item
{...{ props: formItemLayout }}
label='TimePicker'
>
{getFieldDecorator('time-picker', config)(
<a-time-picker />
)}
</a-form-item>
<a-form-item
wrapperCol={{
xs: { span: 24, offset: 0 },
sm: { span: 16, offset: 8 },
}}
>
<a-button type='primary' htmlType='submit'>Submit</a-button>
</a-form-item>
</a-form>
)
},
}
export default Form.create()(TimeRelatedForm)
</script>

View File

@ -0,0 +1,205 @@
<cn>
#### 校验其他组件
以上演示没有出现的表单控件对应的校验演示
</cn>
<us>
#### Other Form Controls
Demostration for validataion configuration for form controls which are not show in the above demos.
</us>
<script>
import { Form } from 'vue-antd-ui'
const Demo = {
methods: {
handleSubmit (e) {
e.preventDefault()
this.form.validateFields((err, values) => {
if (!err) {
console.log('Received values of form: ', values)
}
})
},
normFile (e) {
console.log('Upload event:', e)
if (Array.isArray(e)) {
return e
}
return e && e.fileList
},
},
render () {
const { getFieldDecorator } = this.form
const formItemLayout = {
labelCol: { span: 6 },
wrapperCol: { span: 14 },
}
return (
<a-form id='components-form-demo-validate-other' onSubmit={this.handleSubmit}>
<a-form-item
{...{ props: formItemLayout }}
label='Plain Text'
>
<span class='ant-form-text'>China</span>
</a-form-item>
<a-form-item
{...{ props: formItemLayout }}
label='Select'
hasFeedback
>
{getFieldDecorator('select', {
rules: [
{ required: true, message: 'Please select your country!' },
],
})(
<a-select placeholder='Please select a country'>
<a-select-option value='china'>China</a-select-option>
<a-select-option value='use'>U.S.A</a-select-option>
</a-select>
)}
</a-form-item>
<a-form-item
{...{ props: formItemLayout }}
label='Select[multiple]'
>
{getFieldDecorator('select-multiple', {
rules: [
{ required: true, message: 'Please select your favourite colors!', type: 'array' },
],
})(
<a-select mode='multiple' placeholder='Please select favourite colors'>
<a-select-option value='red'>Red</a-select-option>
<a-select-option value='green'>Green</a-select-option>
<a-select-option value='blue'>Blue</a-select-option>
</a-select>
)}
</a-form-item>
<a-form-item
{...{ props: formItemLayout }}
label='InputNumber'
>
{getFieldDecorator('input-number', { initialValue: 3 })(
<a-input-number min={1} max={10} />
)}
<span class='ant-form-text'> machines</span>
</a-form-item>
<a-form-item
{...{ props: formItemLayout }}
label='Switch'
>
{getFieldDecorator('switch', { valuePropName: 'checked' })(
<a-switch />
)}
</a-form-item>
<a-form-item
{...{ props: formItemLayout }}
label='Slider'
>
{getFieldDecorator('slider')(
<a-slider marks={{ 0: 'A', 20: 'B', 40: 'C', 60: 'D', 80: 'E', 100: 'F' }} />
)}
</a-form-item>
<a-form-item
{...{ props: formItemLayout }}
label='Radio.Group'
>
{getFieldDecorator('radio-group')(
<a-radio-group>
<a-radio value='a'>item 1</a-radio>
<a-radio value='b'>item 2</a-radio>
<a-radio value='c'>item 3</a-radio>
</a-radio-group>
)}
</a-form-item>
<a-form-item
{...{ props: formItemLayout }}
label='Radio.Button'
>
{getFieldDecorator('radio-button')(
<a-radio-group>
<a-radio-button value='a'>item 1</a-radio-button>
<a-radio-button value='b'>item 2</a-radio-button>
<a-radio-button value='c'>item 3</a-radio-button>
</a-radio-group>
)}
</a-form-item>
<a-form-item
{...{ props: formItemLayout }}
label='Rate'
>
{getFieldDecorator('rate', {
initialValue: 3.5,
})(
<a-rate allowHalf/>
)}
</a-form-item>
<a-form-item
{...{ props: formItemLayout }}
label='Upload'
extra='longgggggggggggggggggggggggggggggggggg'
>
{getFieldDecorator('upload', {
valuePropName: 'fileList',
getValueFromEvent: this.normFile,
})(
<a-upload name='logo' action='/upload.do' listType='picture'>
<a-button>
<a-icon type='upload' /> Click to upload
</a-button>
</a-upload>
)}
</a-form-item>
<a-form-item
{...{ props: formItemLayout }}
label='Dragger'
>
<div class='dropbox'>
{getFieldDecorator('dragger', {
valuePropName: 'fileList',
getValueFromEvent: this.normFile,
})(
<a-upload-dragger name='files' action='/upload.do'>
<p class='ant-upload-drag-icon'>
<a-icon type='inbox' />
</p>
<p class='ant-upload-text'>Click or drag file to this area to upload</p>
<p class='ant-upload-hint'>Support for a single or bulk upload.</p>
</a-upload-dragger>
)}
</div>
</a-form-item>
<a-form-item
wrapperCol={{ span: 12, offset: 6 }}
>
<a-button type='primary' htmlType='submit'>Submit</a-button>
</a-form-item>
</a-form>
)
},
}
export default Form.create()(Demo)
</script>
<style>
#components-form-demo-validate-other .dropbox {
height: 180px;
line-height: 1.5;
}
</style>

View File

@ -0,0 +1,178 @@
<cn>
#### 自定义校验
我们提供了 `validateStatus` `help` `hasFeedback` 等属性你可以不需要使用 `Form.create` `getFieldDecorator`自己定义校验的时机和内容
1. `validateStatus`: 校验状态可选 'success', 'warning', 'error', 'validating'
2. `hasFeedback`用于给输入框添加反馈图标
3. `help`设置校验文案
</cn>
<us>
#### Customized Validation
We provide properties like `validateStatus` `help` `hasFeedback` to customize your own validate status and message, without using `Form.create` and `getFieldDecorator`.
1. `validateStatus`: validate status of form components which could be 'success', 'warning', 'error', 'validating'.
2. `hasFeedback`: display feed icon of input control
3. `help`: display validate message.
</us>
<template>
<a-form>
<a-form-item
:labelCol="labelCol"
:wrapperCol="wrapperCol"
label='Fail'
validateStatus='error'
help='Should be combination of numbers & alphabets'
>
<a-input placeholder='unavailable choice' id='error' />
</a-form-item>
<a-form-item
:labelCol="labelCol"
:wrapperCol="wrapperCol"
label='Warning'
validateStatus='warning'
>
<a-input placeholder='Warning' id='warning' />
</a-form-item>
<a-form-item
:labelCol="labelCol"
:wrapperCol="wrapperCol"
label='Validating'
hasFeedback
validateStatus='validating'
help='The information is being validated...'
>
<a-input placeholder="I'm the content is being validated" id='validating' />
</a-form-item>
<a-form-item
:labelCol="labelCol"
:wrapperCol="wrapperCol"
label='Success'
hasFeedback
validateStatus='success'
>
<a-input placeholder="I'm the content" id='success' />
</a-form-item>
<a-form-item
:labelCol="labelCol"
:wrapperCol="wrapperCol"
label='Warning'
hasFeedback
validateStatus='warning'
>
<a-input placeholder='Warning' id='warning' />
</a-form-item>
<a-form-item
:labelCol="labelCol"
:wrapperCol="wrapperCol"
label='Fail'
hasFeedback
validateStatus='error'
help='Should be combination of numbers & alphabets'
>
<a-input placeholder='unavailable choice' id='error' />
</a-form-item>
<a-form-item
:labelCol="labelCol"
:wrapperCol="wrapperCol"
label='Success'
hasFeedback
validateStatus='success'
>
<a-date-picker style="width: 100%" />
</a-form-item>
<a-form-item
:labelCol="labelCol"
:wrapperCol="wrapperCol"
label='Warning'
hasFeedback
validateStatus='warning'
>
<a-time-picker style="width: 100%" />
</a-form-item>
<a-form-item
:labelCol="labelCol"
:wrapperCol="wrapperCol"
label='Error'
hasFeedback
validateStatus='error'
>
<a-select defaultValue='1'>
<a-select-option value='1'>Option 1</a-select-option>
<a-select-option value='2'>Option 2</a-select-option>
<a-select-option value='3'>Option 3</a-select-option>
</a-select>
</a-form-item>
<a-form-item
:labelCol="labelCol"
:wrapperCol="wrapperCol"
label='Validating'
hasFeedback
validateStatus='validating'
help='The information is being validated...'
>
<a-cascader :defaultValue="['1']" :options="[]" />
</a-form-item>
<a-form-item
label='inline'
:labelCol="labelCol"
:wrapperCol="wrapperCol"
>
<a-col :span="11">
<a-form-item validateStatus='error' help='Please select the correct date'>
<a-date-picker style="width: 100%"/>
</a-form-item>
</a-col>
<a-col :span="2">
<span :style="{ display: 'inline-block', width: '100%', textAlign: 'center' }">
-
</span>
</a-col>
<a-col :span="11">
<a-form-item>
<a-date-picker style="width: 100%"/>
</a-form-item>
</a-col>
</a-form-item>
<a-form-item
:labelCol="labelCol"
:wrapperCol="wrapperCol"
label='Success'
hasFeedback
validateStatus='success'
>
<a-input-number style="width: 100%" />
</a-form-item>
</a-form>
</template>
<script>
export default {
data () {
return {
labelCol: {
xs: { span: 24 },
sm: { span: 5 },
},
wrapperCol: {
xs: { span: 24 },
sm: { span: 12 },
},
}
},
}
</script>

View File

@ -0,0 +1,67 @@
<cn>
#### 自行处理表单数据
使用 `Form.create` 处理后的表单具有自动收集数据并校验的功能但如果您不需要这个功能或者默认的行为无法满足业务需求可以选择不使用 `Form.create` 并自行处理数据
</cn>
<us>
#### Handle Form Data Manually
`Form.create` will collect and validate form data automatically. But if you don't need this feature or the default behaviour cannot satisfy your business, you can drop `Form.create` and handle form data manually.
</us>
<template>
<a-form>
<a-form-item
:labelCol="labelCol"
:wrapperCol="wrapperCol"
label="Prime between 8 & 12"
:validateStatus="number.validateStatus"
:help="number.errorMsg || tips"
>
<a-input-number
:min="8"
:max="12"
:value="number.value"
@change="handleNumberChange"
/>
</a-form-item>
</a-form>
</template>
<script>
function validatePrimeNumber (number) {
if (number === 11) {
return {
validateStatus: 'success',
errorMsg: null,
}
}
return {
validateStatus: 'error',
errorMsg: 'The prime between 8 and 12 is 11!',
}
}
export default {
data () {
return {
labelCol: { span: 7 },
wrapperCol: { span: 12 },
number: {
value: 11,
},
tips: 'A prime is a natural number greater than 1 that has no positive divisors other than 1 and itself.',
}
},
methods: {
handleNumberChange (value) {
this.number = {
...validatePrimeNumber(value),
value,
}
},
},
}
</script>

View File

@ -0,0 +1,141 @@
````html
<Form.Item {...props}>
{children}
</Form.Item>
````
## API
### Form
| Property | Description | Type | Default Value |
| -------- | ----------- | ---- | ------------- |
| form | Decorated by `Form.create()` will be automatically set `this.form` property, so just pass to form, you don't need to set it by yourself after 1.7.0. | object | n/a |
| hideRequiredMark | Hide required mark of all form items | Boolean | false |
| layout | Define form layout(Support after 2.8) | 'horizontal'\|'vertical'\|'inline' | 'horizontal' |
### Events
| Events Name | Description | Arguments |
| --- | --- | --- |
| submit | Defines a function will be called if form data validation is successful. | Function(e:Event) |
### Form.create(options)
How to use
```jsx
const CustomizedForm = {}
CustomizedForm = Form.create({})(CustomizedForm);
```
The following `options` are available:
| Property | Description | Type |
| -------- | ----------- | ---- |
| props | declare props on form(和[like vue props]( https://vuejs.org/v2/api/#props)) | {} |
| mapPropsToFields | Convert props to field value(e.g. reading the values from Redux store). And you must mark returned fields with [`Form.createFormField`](#Form.createFormField) | (props) => Object{ fieldName: FormField { value } } |
| validateMessages | Default validate message. And its format is similar with [newMessages](https://github.com/yiminghe/async-validator/blob/master/src/messages.js)'s returned value | Object { [nested.path]&#x3A; String } |
| onFieldsChange | Specify a function that will be called when the value a `Form.Item` gets changed. Usage example: saving the field's value to Redux store. | Function(props, fields) |
| onValuesChange | A handler while value of any field is changed | (props, values) => void |
If the form has been decorated by `Form.create` then it has `this.form` property. `this.form` provides some APIs as follows:
> Note: Before using `getFieldsValue` `getFieldValue` `setFieldsValue` and so on, please make sure that corresponding field had been registered with `getFieldDecorator`.
| Method | Description | Type |
| ------ | ----------- | ---- |
| getFieldDecorator | Two-way binding for form, please read below for details. | |
| getFieldError | Get the error of a field. | Function(name) |
| getFieldsError | Get the specified fields' error. If you don't specify a parameter, you will get all fields' error. | Function(\[names: string\[]]) |
| getFieldsValue | Get the specified fields' values. If you don't specify a parameter, you will get all fields' values. | Function(\[fieldNames: string\[]]) |
| getFieldValue | Get the value of a field. | Function(fieldName: string) |
| isFieldsTouched | Check whether any of fields is touched by `getFieldDecorator`'s `options.trigger` event | (names?: string\[]) => boolean |
| isFieldTouched | Check whether a field is touched by `getFieldDecorator`'s `options.trigger` event | (name: string) => boolean |
| isFieldValidating | Check if the specified field is being validated. | Function(name) |
| resetFields | Reset the specified fields' value(to `initialValue`) and status. If you don't specify a parameter, all the fields will be reset. | Function(\[names: string\[]]) |
| setFields | Set the value and error of a field. | Function({ [fieldName]&#x3A; { value: any, errors: [Error] } }) |
| setFields | | Function(obj: object) |
| setFieldsValue | Set the value of a field. | Function({ [fieldName]&#x3A; value } |
| validateFields | Validate the specified fields and get theirs values and errors. If you don't specify the parameter of fieldNames, you will vaildate all fields. | Function(\[fieldNames: string\[]], [options: object], callback: Function(errors, values)) |
| validateFieldsAndScroll | This function is similar to `validateFields`, but after validation, if the target field is not in visible area of form, form will be automatically scrolled to the target field area. | same as `validateFields` |
### this.form.validateFields/validateFieldsAndScroll(\[fieldNames: string\[]], [options: object], callback: Function(errors, values))
| Method | Description | Type | Default |
| ------ | ----------- | ---- | ------- |
| options.first | If `true`, every field will stop validation at first failed rule | boolean | false |
| options.firstFields | Those fields will stop validation at first failed rule | String\[] | \[] |
| options.force | Should validate validated field again when `validateTrigger` is been triggered again | boolean | false |
| options.scroll | Config scroll behavior of `validateFieldsAndScroll`, more: [dom-scroll-into-view's config](https://github.com/yiminghe/dom-scroll-into-view#function-parameter) | Object | {} |
### Form.createFormField
To mark the returned fields data in `mapPropsToFields`, [demo](#components-form-demo-global-state).
### this.form.getFieldDecorator(id, options)
After wrapped by `getFieldDecorator`, `value`(or other property defined by `valuePropName`) `onChange`(or other property defined by `trigger`) props will be added to form controlsthe flow of form data will be handled by Form which will cause:
1. You shouldn't use `onChange` to collect data, but you still can listen to `onChange`(and so on) events.
2. You can not set value of form control via `value` `defaultValue` prop, and you should set default value with `initialValue` in `getFieldDecorator` instead.
3. You shouldn't call `v-model` manually, please use `this.form.setFieldsValue` to change value programmatically.
#### Special attention
1. `getFieldDecorator` can not be used to decorate stateless component.
2. you can't use `getFieldDecorator` in stateless component: <https://vuejs.org/v2/api/#functional>
#### getFieldDecorator(id, options) parameters
| Property | Description | Type | Default Value |
| -------- | ----------- | ---- | ------------- |
| id | The unique identifier is required. support [nested fields format](https://github.com/react-component/form/pull/48). | string | |
| options.getValueFromEvent | Specify how to get value from event or other onChange arguments | function(..args) | [reference](https://github.com/react-component/form#option-object) |
| options.initialValue | You can specify initial value, type, optional value of children node. (Note: Because `Form` will test equality with `===` internaly, we recommend to use vairable as `initialValue`, instead of literal) | | n/a |
| options.normalize | Normalize value to form component, [a select-all example](https://codepen.io/afc163/pen/JJVXzG?editors=001) | function(value, prevValue, allValues): any | - |
| options.rules | Includes validation rules. Please refer to "Validation Rules" part for details. | object\[] | n/a |
| options.trigger | When to collect the value of children node | string | 'onChange' |
| options.validateFirst | Whether stop validate on first rule of error for this field. | boolean | false |
| options.validateTrigger | When to validate the value of children node. | string\|string\[] | 'onChange' |
| options.valuePropName | Props of children node, for example, the prop of Switch is 'checked'. | string | 'value' |
More option can be referenced at [rc-form option](https://github.com/react-component/form#option-object)。
### Form.Item
Note:
- If Form.Item has multiple children that had been decorated by `getFieldDecorator`, `help` and `required` and `validateStatus` can't be generated automatically.
| Property | Description | Type | Default Value |
| -------- | ----------- | ---- | ------------- |
| colon | Used with `label`, whether to display `:` after label text. | boolean | true |
| extra | The extra prompt message. It is similar to help. Usage example: to display error message and prompt message at the same time. | string\|ReactNode | |
| hasFeedback | Used with `validateStatus`, this option specifies the validation status icon. Recommended to be used only with `Input`. | boolean | false |
| help | The prompt message. If not provided, the prompt message will be generated by the validation rule. | string\|ReactNode | |
| label | Label text | string\|ReactNode | |
| labelCol | The layout of label. You can set `span` `offset` to something like `{span: 3, offset: 12}` or `sm: {span: 3, offset: 12}` same as with `<Col>` | [object](/ant-design/components/grid/#Col) | |
| required | Whether provided or not, it will be generated by the validation rule. | boolean | false |
| validateStatus | The validation status. If not provided, it will be generated by validation rule. options: 'success' 'warning' 'error' 'validating' | string | |
| wrapperCol | The layout for input controls, same as `labelCol` | [object](/ant-design/components/grid/#Col) | |
### Validation Rules
| Property | Description | Type | Default Value |
| -------- | ----------- | ---- | ------------- |
| enum | validate a value from a list of possible values | string | - |
| len | validate an exact length of a field | number | - |
| max | validate a max length of a field | number | - |
| message | validation error message | string | - |
| min | validate a min length of a field | number | - |
| pattern | validate from a regular expression | RegExp | - |
| required | indicates whether field is required | boolean | `false` |
| transform | transform a value before validation | function(value) => transformedValue:any | - |
| type | built-in validation type, [available options](https://github.com/yiminghe/async-validator#type) | string | 'string' |
| validator | custom validate function (Note: [callback must be called](https://github.com/ant-design/ant-design/issues/5155)) | function(rule, value, callback) | - |
| whitespace | treat required fields that only contain whitespace as errors | boolean | `false` |
See more advanced usage at [async-validator](https://github.com/yiminghe/async-validator).

View File

@ -0,0 +1,6 @@
import Form from './Form'
export { FormProps, FormComponentProps, FormCreateOption, ValidateCallback, ValidationRule } from './Form'
export { FormItemProps } from './FormItem'
export default Form

View File

@ -0,0 +1,141 @@
````html
<Form.Item {...props}>
{children}
</Form.Item>
````
## API
### Form
| 参数 | 说明 | 类型 | 默认值 |
| --- | --- | --- | --- |
| form | 经 `Form.create()` 包装过的组件会自带 `this.form` 属性,直接传给 Form 即可。无需手动设置 | object | 无 |
| hideRequiredMark | 隐藏所有表单项的必选标记 | Boolean | false |
| layout | 表单布局 | 'horizontal'\|'vertical'\|'inline' | 'horizontal' |
### 事件
| 事件名称 | 说明 | 回调参数 |
| --- | --- | --- |
| submit | 数据验证成功后回调事件 | Function(e:Event) |
### Form.create(options)
使用方式如下:
```jsx
const CustomizedForm = {}
CustomizedForm = Form.create({})(CustomizedForm);
```
`options` 的配置项如下。
| 参数 | 说明 | 类型 |
| --- | --- | --- |
| props | 父组件需要映射到表单项上的属性声明(和[vue组件props一致]( https://vuejs.org/v2/api/#props)) | {} |
| mapPropsToFields | 把父组件的属性映射到表单项上(如:把 Redux store 中的值读出),需要对返回值中的表单域数据用 [`Form.createFormField`](#Form.createFormField) 标记 | (props) => Object{ fieldName: FormField { value } } |
| validateMessages | 默认校验信息,可用于把默认错误信息改为中文等,格式与 [newMessages](https://github.com/yiminghe/async-validator/blob/master/src/messages.js) 返回值一致 | Object { [nested.path]&#x3A; String } |
| onFieldsChange | 当 `Form.Item` 子节点的值发生改变时触发,可以把对应的值转存到 Redux store | Function(props, fields) |
| onValuesChange | 任一表单域的值发生改变时的回调 | (props, values) => void |
经过 `Form.create` 包装的组件将会自带 `this.form` 属性,`this.form` 提供的 API 如下:
> 注意:使用 `getFieldsValue` `getFieldValue` `setFieldsValue` 等时,应确保对应的 field 已经用 `getFieldDecorator` 注册过了。
| 方法      | 说明                                     | 类型       |
| ------- | -------------------------------------- | -------- |
| getFieldDecorator | 用于和表单进行双向绑定,详见下方描述 | |
| getFieldError | 获取某个输入控件的 Error | Function(name) |
| getFieldsError | 获取一组输入控件的 Error ,如不传入参数,则获取全部组件的 Error | Function(\[names: string\[]]) |
| getFieldsValue | 获取一组输入控件的值,如不传入参数,则获取全部组件的值 | Function(\[fieldNames: string\[]]) |
| getFieldValue | 获取一个输入控件的值 | Function(fieldName: string) |
| isFieldsTouched | 判断是否任一输入控件经历过 `getFieldDecorator` 的值收集时机 `options.trigger` | (names?: string\[]) => boolean |
| isFieldTouched | 判断一个输入控件是否经历过 `getFieldDecorator` 的值收集时机 `options.trigger` | (name: string) => boolean |
| isFieldValidating | 判断一个输入控件是否在校验状态 | Function(name) |
| resetFields | 重置一组输入控件的值(为 `initialValue`)与状态,如不传入参数,则重置所有组件 | Function(\[names: string\[]]) |
| setFields | 设置一组输入控件的值与 Error。 | Function({ [fieldName]&#x3A; { value: any, errors: [Error] } }) |
| setFieldsValue | 设置一组输入控件的值 | Function({ [fieldName]&#x3A; value } |
| validateFields | 校验并获取一组输入域的值与 Error若 fieldNames 参数为空,则校验全部组件 | Function(\[fieldNames: string\[]], [options: object], callback: Function(errors, values)) |
| validateFieldsAndScroll | 与 `validateFields` 相似,但校验完后,如果校验不通过的菜单域不在可见范围内,则自动滚动进可见范围 | 参考 `validateFields` |
### this.form.validateFields/validateFieldsAndScroll(\[fieldNames: string\[]], [options: object], callback: Function(errors, values))
| 参数 | 说明 | 类型 | 默认值 |
| --- | --- | --- | --- |
| options.first | 若为 true则每一表单域的都会在碰到第一个失败了的校验规则后停止校验 | boolean | false |
| options.firstFields | 指定表单域会在碰到第一个失败了的校验规则后停止校验 | String\[] | \[] |
| options.force | 对已经校验过的表单域,在 validateTrigger 再次被触发时是否再次校验 | boolean | false |
| options.scroll | 定义 validateFieldsAndScroll 的滚动行为,详细配置见 [dom-scroll-into-view config](https://github.com/yiminghe/dom-scroll-into-view#function-parameter) | Object | {} |
### Form.createFormField
用于标记 `mapPropsToFields` 返回的表单域数据,[例子](#components-form-demo-global-state)。
### this.form.getFieldDecorator(id, options)
经过 `getFieldDecorator` 包装的控件,表单控件会自动添加 `value`(或 `valuePropName` 指定的其他属性) `onChange`(或 `trigger` 指定的其他属性),数据同步将被 Form 接管,这会导致以下结果:
1. 你**不再需要也不应该**用 `onChange` 来做同步,但还是可以继续监听 `onChange` 等事件。
2. 你不能用控件的 `value` `defaultValue` 等属性来设置表单域的值,默认值可以用 `getFieldDecorator` 里的 `initialValue`
3. 你不应该用 `v-modle`,可以使用 `this.form.setFieldsValue` 来动态改变表单值。
#### 特别注意
1. `getFieldDecorator` 不能用于装饰纯函数组件。
2. `getFieldDecorator` 调用不能位于纯函数组件中 <https://cn.vuejs.org/v2/api/#functional>
#### getFieldDecorator(id, options) 参数
| 参数 | 说明 | 类型 | 默认值 |
| --- | --- | --- | --- |
| id | 必填输入控件唯一标志。支持嵌套式的写法。 | string | |
| options.getValueFromEvent | 可以把 onChange 的参数(如 event转化为控件的值 | function(..args) | [reference](https://github.com/react-component/form#option-object) |
| options.initialValue | 子节点的初始值,类型、可选值均由子节点决定(注意:由于内部校验时使用 `===` 判断是否变化,建议使用变量缓存所需设置的值而非直接使用字面量)) | | |
| options.normalize | 转换默认的 value 给控件,[一个选择全部的例子](https://codepen.io/afc163/pen/JJVXzG?editors=001) | function(value, prevValue, allValues): any | - |
| options.rules | 校验规则,参考下方文档 | object\[] | |
| options.trigger | 收集子节点的值的时机 | string | 'onChange' |
| options.validateFirst | 当某一规则校验不通过时,是否停止剩下的规则的校验 | boolean | false |
| options.validateTrigger | 校验子节点值的时机 | string\|string\[] | 'onChange' |
| options.valuePropName | 子节点的值的属性,如 Switch 的是 'checked' | string | 'value' |
更多参数可参考 [rc-form option](https://github.com/react-component/form#option-object)。
### Form.Item
注意:
- 一个 Form.Item 建议只放一个被 getFieldDecorator 装饰过的 child当有多个被装饰过的 child 时,`help` `required` `validateStatus` 无法自动生成。
| 参数 | 说明 | 类型 | 默认值 |
| --- | --- | --- | --- |
| colon | 配合 label 属性使用,表示是否显示 label 后面的冒号 | boolean | true |
| extra | 额外的提示信息,和 help 类似,当需要错误信息和提示文案同时出现时,可以使用这个。 | string\|ReactNode | |
| hasFeedback | 配合 validateStatus 属性使用,展示校验状态图标,建议只配合 Input 组件使用 | boolean | false |
| help | 提示信息,如不设置,则会根据校验规则自动生成 | string\|ReactNode | |
| label | label 标签的文本 | string\|ReactNode | |
| labelCol | label 标签布局,同 `<Col>` 组件,设置 `span` `offset` 值,如 `{span: 3, offset: 12}``sm: {span: 3, offset: 12}` | [object](/ant-design/components/grid-cn/#Col) | |
| required | 是否必填,如不设置,则会根据校验规则自动生成 | boolean | false |
| validateStatus | 校验状态,如不设置,则会根据校验规则自动生成,可选:'success' 'warning' 'error' 'validating' | string | |
| wrapperCol | 需要为输入控件设置布局样式时,使用该属性,用法同 labelCol | [object](/ant-design/components/grid-cn/#Col) | |
### 校验规则
| 参数 | 说明 | 类型 | 默认值 |
| --- | --- | --- | --- |
| enum | 枚举类型 | string | - |
| len | 字段长度 | number | - |
| max | 最大长度 | number | - |
| message | 校验文案 | string | - |
| min | 最小长度 | number | - |
| pattern | 正则表达式校验 | RegExp | - |
| required | 是否必选 | boolean | `false` |
| transform | 校验前转换字段值 | function(value) => transformedValue:any | - |
| type | 内建校验类型,[可选项](https://github.com/yiminghe/async-validator#type) | string | 'string' |
| validator | 自定义校验(注意,[callback 必须被调用](https://github.com/ant-design/ant-design/issues/5155) | function(rule, value, callback) | - |
| whitespace | 必选时,空格是否会被视为错误 | boolean | `false` |
更多高级用法可研究 [async-validator](https://github.com/yiminghe/async-validator)。

View File

@ -0,0 +1,5 @@
import '../../style/index.less'
import './index.less'
// style dependencies
import '../../grid/style'

View File

@ -0,0 +1,624 @@
@import "../../style/themes/default";
@import "../../style/mixins/index";
@import "../../input/style/mixin";
@import "../../button/style/mixin";
@import "../../grid/style/mixin";
@import "./mixin";
@form-prefix-cls: ~"@{ant-prefix}-form";
@form-component-height: @input-height-base;
@form-component-max-height: @input-height-lg;
@form-feedback-icon-size: 14px;
@form-help-margin-top: (@form-component-height - @form-component-max-height) / 2 + 2px;
.@{form-prefix-cls} {
.reset-component;
.reset-form;
}
.@{form-prefix-cls}-item-required:before {
display: inline-block;
margin-right: 4px;
content: "*";
font-family: SimSun;
line-height: 1;
font-size: @font-size-base;
color: @label-required-color;
.@{form-prefix-cls}-hide-required-mark & {
display: none;
}
}
// Radio && Checkbox
input[type="radio"],
input[type="checkbox"] {
&[disabled],
&.disabled {
cursor: not-allowed;
}
}
// These classes are used directly on <label>s
.@{ant-prefix}-radio-inline,
.@{ant-prefix}-radio-vertical,
.@{ant-prefix}-checkbox-inline,
.@{ant-prefix}-checkbox-vertical {
&.disabled {
cursor: not-allowed;
}
}
// These classes are used on elements with <label> descendants
.@{ant-prefix}-radio,
.@{ant-prefix}-checkbox {
&.disabled {
label {
cursor: not-allowed;
}
}
}
// Form items
// You should wrap labels and controls in .@{form-prefix-cls}-item for optimum spacing
.@{form-prefix-cls}-item {
label {
position: relative;
> .@{iconfont-css-prefix} {
vertical-align: top;
font-size: @font-size-base;
}
}
.reset-component;
margin-bottom: @form-item-margin-bottom;
vertical-align: top;
// nested FormItem
&-control > &:last-child,
& [class^="@{ant-prefix}-col-"] > &:only-child {
margin-bottom: -@form-item-margin-bottom;
}
&-control {
line-height: @form-component-max-height - 0.0001px; // https://github.com/ant-design/ant-design/issues/8220
position: relative;
.clearfix;
}
&-children {
position: relative;
}
&-with-help {
margin-bottom: @form-item-margin-bottom - @font-size-base * @line-height-base - @form-help-margin-top;
}
&-label {
text-align: right;
vertical-align: middle;
line-height: @form-component-max-height - 0.0001px;
display: inline-block;
overflow: hidden;
white-space: nowrap;
label {
color: @label-color;
&:after {
& when (@form-item-trailing-colon=true) {
content: ":";
}
& when not (@form-item-trailing-colon=true) {
content: " ";
}
margin: 0 8px 0 2px;
position: relative;
top: -0.5px;
}
}
}
.@{ant-prefix}-switch {
margin: 2px 0 4px;
}
&-no-colon &-label label:after {
content: " ";
}
}
.@{form-prefix-cls}-explain,
.@{form-prefix-cls}-extra {
color: @text-color-secondary;
line-height: @line-height-base;
transition: color .15s @ease-out;
margin-top: @form-help-margin-top;
}
.@{form-prefix-cls}-extra {
padding-top: 4px;
}
.@{form-prefix-cls}-text {
display: inline-block;
padding-right: 8px;
}
.@{form-prefix-cls}-split {
display: block;
text-align: center;
}
// 表单下的输入框尺寸唯一: 大尺寸
form {
.has-feedback {
.@{ant-prefix}-input {
padding-right: 24px;
}
// Fix overlapping between feedback icon and <Select>'s arrow.
// https://github.com/ant-design/ant-design/issues/4431
> .@{ant-prefix}-select .@{ant-prefix}-select-arrow,
> .@{ant-prefix}-select .@{ant-prefix}-select-selection__clear,
:not(.@{ant-prefix}-input-group-addon) > .@{ant-prefix}-select .@{ant-prefix}-select-arrow,
:not(.@{ant-prefix}-input-group-addon) > .@{ant-prefix}-select .@{ant-prefix}-select-selection__clear {
right: 28px;
}
> .@{ant-prefix}-select .@{ant-prefix}-select-selection-selected-value,
:not(.@{ant-prefix}-input-group-addon) > .@{ant-prefix}-select .@{ant-prefix}-select-selection-selected-value {
padding-right: 42px;
}
.@{ant-prefix}-cascader-picker {
&-arrow {
margin-right: 17px;
}
&-clear {
right: 28px;
}
}
// Fix issue: https://github.com/ant-design/ant-design/issues/7854
.@{ant-prefix}-input-search:not(.@{ant-prefix}-input-search-enter-button) {
.@{ant-prefix}-input-suffix {
right: 28px;
}
}
// Fix issue: https://github.com/ant-design/ant-design/issues/4783
.@{ant-prefix}-calendar-picker,
.@{ant-prefix}-time-picker {
&-icon,
&-clear {
right: 28px;
}
}
}
textarea.@{ant-prefix}-input {
height: auto;
}
// input[type=file]
.@{ant-prefix}-upload {
background: transparent;
}
input[type="radio"],
input[type="checkbox"] {
width: 14px;
height: 14px;
}
// Radios and checkboxes on same line
.@{ant-prefix}-radio-inline,
.@{ant-prefix}-checkbox-inline {
display: inline-block;
vertical-align: middle;
font-weight: normal;
cursor: pointer;
margin-left: 8px;
&:first-child {
margin-left: 0;
}
}
.@{ant-prefix}-checkbox-vertical,
.@{ant-prefix}-radio-vertical {
display: block;
}
.@{ant-prefix}-checkbox-vertical + .@{ant-prefix}-checkbox-vertical,
.@{ant-prefix}-radio-vertical + .@{ant-prefix}-radio-vertical {
margin-left: 0;
}
.@{ant-prefix}-input-number + .@{form-prefix-cls}-text {
margin-left: 8px;
}
.@{ant-prefix}-select,
.@{ant-prefix}-cascader-picker {
width: 100%;
}
// Don't impact select inside input group
.@{ant-prefix}-input-group .@{ant-prefix}-select,
.@{ant-prefix}-input-group .@{ant-prefix}-cascader-picker {
width: auto;
}
// fix input with addon position. https://github.com/ant-design/ant-design/issues/8243
.@{ant-prefix}-input-group-wrapper {
vertical-align: middle;
position: relative;
top: -1px;
}
}
// Input combined with select
.@{ant-prefix}-input-group-wrap {
.@{ant-prefix}-select-selection {
border-bottom-left-radius: 0;
border-top-left-radius: 0;
&:hover {
border-color: @border-color-base;
}
}
.@{ant-prefix}-select-selection--single {
margin-left: -1px;
height: @input-height-lg;
background-color: #eee;
.@{ant-prefix}-select-selection__rendered {
padding-left: 8px;
padding-right: 25px;
line-height: 30px;
}
}
.@{ant-prefix}-select-open .@{ant-prefix}-select-selection {
border-color: @border-color-base;
box-shadow: none;
}
}
// Form layout
//== Vertical Form
.make-vertical-layout-label() {
padding: @form-vertical-label-padding;
margin: @form-vertical-label-margin;
display: block;
text-align: left;
line-height: @line-height-base;
label:after {
display: none;
}
}
.make-vertical-layout() {
.@{form-prefix-cls}-item-label,
.@{form-prefix-cls}-item-control-wrapper {
display: block;
width: 100%;
}
.@{form-prefix-cls}-item-label {
.make-vertical-layout-label();
}
}
.@{form-prefix-cls}-vertical .@{form-prefix-cls}-item-label,
// when labelCol is 24, it is a vertical form
.@{ant-prefix}-col-24.@{form-prefix-cls}-item-label,
.@{ant-prefix}-col-xl-24.@{form-prefix-cls}-item-label {
.make-vertical-layout-label();
}
.@{form-prefix-cls}-vertical {
.@{form-prefix-cls}-item {
padding-bottom: 8px;
}
.@{form-prefix-cls}-item-control {
line-height: @line-height-base;
}
.@{form-prefix-cls}-explain,
.@{form-prefix-cls}-extra {
margin-top: 2px;
margin-bottom: -4px;
}
}
@media (max-width: @screen-xs-max) {
.make-vertical-layout();
.@{ant-prefix}-col-xs-24.@{form-prefix-cls}-item-label {
.make-vertical-layout-label();
}
}
@media (max-width: @screen-sm-max) {
.@{ant-prefix}-col-sm-24.@{form-prefix-cls}-item-label {
.make-vertical-layout-label();
}
}
@media (max-width: @screen-md-max) {
.@{ant-prefix}-col-md-24.@{form-prefix-cls}-item-label {
.make-vertical-layout-label();
}
}
@media (max-width: @screen-lg-max) {
.@{ant-prefix}-col-lg-24.@{form-prefix-cls}-item-label {
.make-vertical-layout-label();
}
}
@media (max-width: @screen-xl-max) {
.@{ant-prefix}-col-xl-24.@{form-prefix-cls}-item-label {
.make-vertical-layout-label();
}
}
//== Inline Form
.@{form-prefix-cls}-inline {
.@{form-prefix-cls}-item {
display: inline-block;
margin-right: 16px;
margin-bottom: 0;
&-with-help {
margin-bottom: 24px;
}
> .@{form-prefix-cls}-item-control-wrapper, > .@{form-prefix-cls}-item-label {
display: inline-block;
vertical-align: middle;
}
}
.@{form-prefix-cls}-text {
display: inline-block;
}
.has-feedback {
display: inline-block;
}
// Fix https://github.com/ant-design/ant-design/issues/1040
.@{form-prefix-cls}-explain {
position: absolute;
}
}
// Validation state
.has-success,
.has-warning,
.has-error,
.is-validating {
&.has-feedback .@{form-prefix-cls}-item-children:after {
position: absolute;
top: 50%;
right: 0;
visibility: visible;
pointer-events: none;
width: @form-component-height;
height: 20px;
line-height: 20px;
margin-top: -10px;
text-align: center;
font-size: @form-feedback-icon-size;
animation: zoomIn .3s @ease-out-back;
.iconfont-font("");
z-index: 1;
}
}
.has-success {
&.has-feedback .@{form-prefix-cls}-item-children:after {
animation-name: diffZoomIn1 !important;
content: '\e630';
color: @success-color;
}
}
.has-warning {
.form-control-validation(@warning-color; @warning-color;);
&.has-feedback .@{form-prefix-cls}-item-children:after {
content: '\e62c';
color: @warning-color;
animation-name: diffZoomIn3 !important;
}
//select
.@{ant-prefix}-select {
&-selection {
border-color: @warning-color;
}
&-open .@{ant-prefix}-select-selection,
&-focused .@{ant-prefix}-select-selection {
.active(@warning-color);
}
}
// arrow and icon
.@{ant-prefix}-calendar-picker-icon:after,
.@{ant-prefix}-time-picker-icon:after,
.@{ant-prefix}-picker-icon:after,
.@{ant-prefix}-select-arrow,
.@{ant-prefix}-cascader-picker-arrow {
color: @warning-color;
}
//input-number, timepicker
.@{ant-prefix}-input-number,
.@{ant-prefix}-time-picker-input {
border-color: @warning-color;
&-focused,
&:focus {
.active(@warning-color);
}
&:not([disabled]):hover {
border-color: @warning-color;
}
}
.@{ant-prefix}-cascader-picker:focus .@{ant-prefix}-cascader-input {
.active(@warning-color);
}
}
.has-error {
.form-control-validation(@error-color; @error-color;);
&.has-feedback .@{form-prefix-cls}-item-children:after {
content: '\e62e';
color: @error-color;
animation-name: diffZoomIn2 !important;
}
//select
.@{ant-prefix}-select {
&-selection {
border-color: @error-color;
}
&-open .@{ant-prefix}-select-selection,
&-focused .@{ant-prefix}-select-selection {
.active(@error-color);
}
}
.@{ant-prefix}-select.@{ant-prefix}-select-auto-complete {
.@{ant-prefix}-input:focus {
border-color: @error-color;
}
}
.@{ant-prefix}-input-group-addon .@{ant-prefix}-select {
&-selection {
border-color: transparent;
box-shadow: none;
}
}
// arrow and icon
.@{ant-prefix}-calendar-picker-icon:after,
.@{ant-prefix}-time-picker-icon:after,
.@{ant-prefix}-picker-icon:after,
.@{ant-prefix}-select-arrow,
.@{ant-prefix}-cascader-picker-arrow {
color: @error-color;
}
//input-number, timepicker
.@{ant-prefix}-input-number,
.@{ant-prefix}-time-picker-input {
border-color: @error-color;
&-focused,
&:focus {
.active(@error-color);
}
&:not([disabled]):hover {
border-color: @error-color;
}
}
.@{ant-prefix}-mention-wrapper {
.@{ant-prefix}-mention-editor {
&,
&:not([disabled]):hover {
border-color: @error-color;
}
}
&.@{ant-prefix}-mention-active:not([disabled]) .@{ant-prefix}-mention-editor,
.@{ant-prefix}-mention-editor:not([disabled]):focus {
.active(@error-color);
}
}
.@{ant-prefix}-cascader-picker:focus .@{ant-prefix}-cascader-input {
.active(@error-color);
}
}
.is-validating {
&.has-feedback .@{form-prefix-cls}-item-children:after {
display: inline-block;
animation: loadingCircle 1s infinite linear;
content: "\e64d";
color: @primary-color;
}
}
.@{ant-prefix}-advanced-search-form {
.@{form-prefix-cls}-item {
margin-bottom: @form-item-margin-bottom;
&-with-help {
margin-bottom: @form-item-margin-bottom - @font-size-base * @line-height-base - @form-help-margin-top;
}
}
}
.show-help-motion(@className, @keyframeName, @duration: @animation-duration-slow) {
.make-motion(@className, @keyframeName, @duration);
.@{className}-enter,
.@{className}-appear {
opacity: 0;
animation-timing-function: @ease-in-out;
}
.@{className}-leave {
animation-timing-function: @ease-in-out;
}
}
.show-help-motion(show-help, antShowHelp, 0.15s);
@keyframes antShowHelpIn {
0% {
opacity: 0;
transform: translateY(-5px);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
@keyframes antShowHelpOut {
to {
opacity: 0;
transform: translateY(-5px);
}
}
// need there different zoom animation
// otherwise won't trigger anim
@keyframes diffZoomIn1 {
0% {
transform: scale(0);
}
100% {
transform: scale(1);
}
}
@keyframes diffZoomIn2 {
0% {
transform: scale(0);
}
100% {
transform: scale(1);
}
}
@keyframes diffZoomIn3 {
0% {
transform: scale(0);
}
100% {
transform: scale(1);
}
}

View File

@ -0,0 +1,106 @@
@import "../../input/style/mixin";
.form-control-validation(@text-color: @input-color; @border-color: @input-border-color; @background-color: @input-bg) {
.@{ant-prefix}-form-explain,
.@{ant-prefix}-form-split {
color: @text-color;
}
// 输入框的不同校验状态
.@{ant-prefix}-input {
&,
&:hover {
border-color: @border-color;
}
&:focus {
.active(@border-color);
}
&:not([disabled]):hover {
border-color: @border-color;
}
}
.@{ant-prefix}-calendar-picker-open .@{ant-prefix}-calendar-picker-input {
.active(@border-color);
}
.@{ant-prefix}-input-prefix {
color: @text-color;
}
.@{ant-prefix}-input-group-addon {
color: @text-color;
border-color: @border-color;
background-color: @background-color;
}
.has-feedback {
color: @text-color;
}
}
// Reset form styles
// -----------------------------
// Based on Bootstrap framework
.reset-form() {
legend {
display: block;
width: 100%;
padding: 0;
margin-bottom: 20px;
font-size: @font-size-lg;
line-height: inherit;
color: @text-color-secondary;
border: 0;
border-bottom: @border-width-base @border-style-base @border-color-base;
}
label {
font-size: @font-size-base;
}
input[type="search"] {
box-sizing: border-box;
}
// Position radios and checkboxes better
input[type="radio"],
input[type="checkbox"] {
line-height: normal;
}
input[type="file"] {
display: block;
}
// Make range inputs behave like textual form controls
input[type="range"] {
display: block;
width: 100%;
}
// Make multiple select elements height not fixed
select[multiple],
select[size] {
height: auto;
}
// Focus for file, radio, and checkbox
input[type="file"]:focus,
input[type="radio"]:focus,
input[type="checkbox"]:focus {
outline: thin dotted;
outline: 5px auto -webkit-focus-ring-color; // lesshint duplicateProperty: false
outline-offset: -2px;
}
// Adjust output element
output {
display: block;
padding-top: 15px;
font-size: @font-size-base;
line-height: @line-height-base;
color: @input-color;
}
}

View File

@ -50,7 +50,7 @@ import { default as Divider } from './divider'
import { default as Dropdown } from './dropdown'
// import { default as Form } from './form'
import { default as Form } from './form'
import { default as Icon } from './icon'
@ -148,6 +148,8 @@ const components = [
Divider,
Dropdown,
Dropdown.Button,
Form,
Form.Item,
Icon,
Input,
Input.Group,
@ -237,6 +239,7 @@ export {
DatePicker,
Divider,
Dropdown,
Form,
Icon,
Input,
InputNumber,

View File

@ -7,7 +7,10 @@ export const InputNumberProps = {
prefixCls: PropTypes.string,
min: PropTypes.number,
max: PropTypes.number,
value: PropTypes.number,
value: PropTypes.oneOfType([
PropTypes.number,
PropTypes.string,
]),
step: PropTypes.oneOfType([
PropTypes.number,
PropTypes.string,

View File

@ -1,8 +1,8 @@
import classNames from 'classnames'
import TextArea from './TextArea'
import omit from 'omit.js'
import inputProps from './inputProps'
import { hasProp, getComponentFromProp } from '../_util/props-util'
import { hasProp, getComponentFromProp, getStyle, getClass } from '../_util/props-util'
function fixControlledValue (value) {
if (typeof value === 'undefined' || value === null) {
@ -12,6 +12,7 @@ function fixControlledValue (value) {
}
export default {
inheritAttrs: false,
name: 'AInput',
props: {
...inputProps,
@ -96,10 +97,15 @@ export default {
[wrapperClassName]: (addonBefore || addonAfter),
}
const groupClassName = classNames(`${props.prefixCls}-group-wrapper`, {
[`${props.prefixCls}-group-wrapper-sm`]: props.size === 'small',
[`${props.prefixCls}-group-wrapper-lg`]: props.size === 'large',
})
if (addonBefore || addonAfter) {
return (
<span
class={`${props.prefixCls}-group-wrapper`}
class={groupClassName}
style={getStyle(this)}
>
<span class={className}>
{addonBefore}
@ -118,7 +124,7 @@ export default {
)
},
renderLabeledIcon (children) {
const { prefixCls } = this.$props
const { prefixCls, size } = this.$props
let prefix = getComponentFromProp(this, 'prefix')
let suffix = getComponentFromProp(this, 'suffix')
if (!prefix && !suffix) {
@ -136,10 +142,14 @@ export default {
{suffix}
</span>
) : null
const affixWrapperCls = classNames(getClass(this), `${prefixCls}-affix-wrapper`, {
[`${prefixCls}-affix-wrapper-sm`]: size === 'small',
[`${prefixCls}-affix-wrapper-lg`]: size === 'large',
})
return (
<span
class={`${prefixCls}-affix-wrapper`}
class={affixWrapperCls}
style={getStyle(this)}
>
{prefix}
{children}
@ -167,7 +177,7 @@ export default {
keydown: handleKeyDown,
input: handleChange,
},
class: getInputClassName(),
class: classNames(getInputClassName(), getClass(this)),
ref: 'input',
}
return this.renderLabeledIcon(

View File

@ -11,7 +11,6 @@ export default {
default: 'text',
type: String,
},
id: [String, Number],
name: String,
size: {
validator (value) {

View File

@ -11,7 +11,7 @@
<template>
<a-locale-provider :locale="fr_FR">
<App />
<a-locale-provider>
</a-locale-provider>
</template>
<script>
import fr_FR from 'antd/lib/locale-provider/fr_FR';
@ -34,7 +34,7 @@ Note: if you need to use antd's UMD dist file, please use `antd/dist/antd-with-l
<template>
<a-locale-provider :locale="locales.fr_FR">
<App />
<a-locale-provider>
</a-locale-provider>
</template>
<script>
const { LocaleProvider, locales } = window.antd;

View File

@ -10,7 +10,7 @@ LocaleProvider 使用 Vue 的 [provide/inject](https://cn.vuejs.org/v2/api/#prov
<template>
<a-locale-provider :locale="zh_CN">
<App />
<a-locale-provider>
</a-locale-provider>
</template>
<script>
import zh_CN from 'antd/lib/locale-provider/zh_CN';
@ -33,7 +33,7 @@ export default {
<template>
<a-locale-provider :locale="locales.en_US">
<App />
<a-locale-provider>
</a-locale-provider>
</template>
<script>
const { LocaleProvider, locales } = window.antd;

View File

@ -231,6 +231,9 @@ export default {
menuProps.on.click = this.handleClick
menuProps.props.openTransitionName = menuOpenAnimation
} else {
menuProps.on.click = (e) => {
this.$emit('click', e)
}
menuProps.props.openAnimation = menuOpenAnimation
}

View File

@ -1,5 +1,9 @@
import Vue from 'vue'
import ConfirmDialog from './ConfirmDialog'
import antRefDirective from '../_util/antRefDirective'
Vue.use(antRefDirective)
export default function confirm (config) {
const div = document.createElement('div')
const el = document.createElement('div')

View File

@ -54,14 +54,14 @@ export default {
handleChange (event) {
const targetChecked = event.target.checked
this.$emit('input', targetChecked)
const { name, value, radioGroupContext, stateChecked } = this
const { name, value, radioGroupContext } = this
if ((!hasProp(this, 'checked') && !radioGroupContext) || (radioGroupContext && radioGroupContext.value === undefined)) {
this.stateChecked = targetChecked
}
const target = {
name,
value,
checked: !stateChecked,
checked: targetChecked,
}
if (this.radioGroupContext) {
this.radioGroupContext.handleChange({ target })

View File

@ -43,3 +43,4 @@ import './transfer/style'
import './tree/style'
import './upload/style'
import './layout/style'
import './form/style'

View File

@ -51,7 +51,7 @@ export default function createTableRow (Component = 'tr') {
}
return (
<Component class={className}>
<Component class={className} {...{ on: this.$listeners }}>
{this.$slots.default}
</Component>
)

View File

@ -10,6 +10,8 @@ import Popup from './Popup'
import { getAlignFromPlacement, getPopupClassNameFromAlign, noop } from './utils'
import BaseMixin from '../_util/BaseMixin'
import { cloneElement } from '../_util/vnode'
import antRefDirective from '../_util/antRefDirective'
Vue.use(antRefDirective)
function returnEmptyString () {
return ''

View File

@ -3,7 +3,7 @@ import Upload from './Upload'
import { UploadProps } from './interface'
export default {
name: 'ADragger',
name: 'AUploadDragger',
props: UploadProps,
render () {
const props = getOptionProps(this)

View File

@ -11,13 +11,13 @@ Classic mode. File selection dialog pops up when upload button is clicked.
```html
<template>
<a-dragger name="file" :multiple="true" action="//jsonplaceholder.typicode.com/posts/" @change="handleChange">
<a-upload-dragger name="file" :multiple="true" action="//jsonplaceholder.typicode.com/posts/" @change="handleChange">
<p class="ant-upload-drag-icon">
<a-icon type="inbox" />
</p>
<p class="ant-upload-text">Click or drag file to this area to upload</p>
<p class="ant-upload-hint">Support for a single or bulk upload. Strictly prohibit from uploading company data or other band files</p>
</a-dragger>
</a-upload-dragger>
</template>
<script>
export default {

View File

@ -0,0 +1 @@
export { default } from './src/'

View File

@ -0,0 +1,130 @@
import PropTypes from '../../_util/vue-types'
import classNames from 'classnames'
import { getOptionProps, hasProp, initDefaultProps } from '../../_util/props-util'
import BaseMixin from '../../_util/BaseMixin'
export default {
name: 'Checkbox',
mixins: [BaseMixin],
props: initDefaultProps({
prefixCls: PropTypes.string,
name: PropTypes.string,
id: PropTypes.string,
type: PropTypes.string,
defaultChecked: PropTypes.oneOfType([PropTypes.number, PropTypes.bool]),
checked: PropTypes.oneOfType([PropTypes.number, PropTypes.bool]),
disabled: PropTypes.bool,
// onFocus: PropTypes.func,
// onBlur: PropTypes.func,
// onChange: PropTypes.func,
// onClick: PropTypes.func,
// tabIndex: PropTypes.string,
// readOnly: PropTypes.bool,
// autoFocus: PropTypes.bool,
value: PropTypes.any,
}, {
prefixCls: 'rc-checkbox',
type: 'checkbox',
defaultChecked: false,
}),
data () {
const checked = hasProp(this, 'checked') ? this.checked : this.defaultChecked
return {
sChecked: checked,
}
},
watch: {
checked (val) {
this.sChecked = val
},
},
methods: {
focus () {
this.$refs.input.focus()
},
blur () {
this.$refs.input.blur()
},
handleChange (e) {
const props = getOptionProps(this)
if (props.disabled) {
return
}
if (!('checked' in props)) {
this.sChecked = e.target.checked
}
props.onChange({
target: {
...props,
checked: e.target.checked,
},
stopPropagation () {
e.stopPropagation()
},
preventDefault () {
e.preventDefault()
},
nativeEvent: e.nativeEvent,
})
},
},
render () {
const {
prefixCls,
name,
id,
type,
disabled,
readOnly,
tabIndex,
onClick,
onFocus,
onBlur,
autoFocus,
value,
...others
} = this
const globalProps = Object.keys(others).reduce((prev, key) => {
if (key.substr(0, 5) === 'aria-' || key.substr(0, 5) === 'data-' || key === 'role') {
prev[key] = others[key]
}
return prev
}, {})
const { checked } = this.state
const classString = classNames(prefixCls, {
[`${prefixCls}-checked`]: checked,
[`${prefixCls}-disabled`]: disabled,
})
return (
<span class={classString}>
<input
name={name}
id={id}
type={type}
readOnly={readOnly}
disabled={disabled}
tabIndex={tabIndex}
class={`${prefixCls}-input`}
checked={!!checked}
onClick={onClick}
onFocus={onFocus}
onBlur={onBlur}
onChange={this.handleChange}
autoFocus={autoFocus}
ref={this.saveInput}
value={value}
{...globalProps}
/>
<span class={`${prefixCls}-inner`} />
</span>
)
},
}

View File

@ -0,0 +1,3 @@
import Checkbox from './Checkbox'
export default Checkbox

View File

@ -0,0 +1,123 @@
/* eslint react/no-multi-comp:0, no-console:0 */
import { createForm } from '../index'
import { regionStyle, errorStyle } from './styles'
import BaseMixin from '../../_util/BaseMixin'
const Email = {
props: {
form: Object,
},
methods: {
checkSpecial (rule, value, callback) {
setTimeout(() => {
if (value === 'yiminghe@gmail.com') {
callback('can not be!')
} else {
callback()
}
}, 1000)
},
},
render () {
const { getFieldProps, getFieldError, isFieldValidating } = this.form
const errors = getFieldError('email')
return (<div style={ regionStyle }>
<div>email validate onBlur</div>
<div>
<input {...getFieldProps('email', {
initialValue: '',
validateFirst: true,
rules: [
{
required: true,
},
{
type: 'email',
message: '错误的 email 格式',
},
this.checkSpecial,
],
validateTrigger: 'blur',
})}
/></div>
<div style={errorStyle}>
{errors ? errors.join(',') : null}
</div>
<div style={errorStyle}>
{isFieldValidating('email') ? 'validating' : null}
</div>
</div>)
},
}
const Form = {
mixins: [BaseMixin],
props: {
form: Object,
},
data () {
return {
loading: true,
}
},
mounted () {
setTimeout(() => {
this.setState({
loading: false,
}, () => {
setTimeout(() => {
this.form.setFieldsInitialValue({
email: 'xx@gmail.com',
})
}, 1000)
})
}, 1000)
},
methods: {
onSubmit (e) {
e.preventDefault()
this.form.submit((callback) => {
setTimeout(() => {
this.form.validateFields((error, values) => {
if (!error) {
console.log('ok', values)
} else {
console.log('error', error, values)
}
callback()
})
}, 1000)
})
},
reset (e) {
e.preventDefault()
this.form.resetFields()
},
},
render () {
if (this.loading) {
return <b>loading</b>
}
const { form } = this
const disabled = form.isFieldsValidating() || form.isSubmitting()
return (<div style={{ margin: 20 }}>
<h2>async init field</h2>
<form onSubmit={this.onSubmit}>
<Email form={ form }/>
<div style={ regionStyle }>
<button disabled={disabled} type='submit'>submit</button>
&nbsp;{disabled ? <span style={{ color: 'red' }}>disabled</span> : null}&nbsp;
<button disabled={disabled} onClick={this.reset}>reset</button>
</div>
</form>
</div>)
},
}
export default createForm()(Form)

View File

@ -0,0 +1,201 @@
/* eslint react/no-multi-comp:0, no-console:0 */
import { createForm } from '../index'
import BaseMixin from '../../_util/BaseMixin'
const Form1 = {
mixins: [BaseMixin],
props: {
form: Object,
},
data () {
return {
useInput: true,
}
},
methods: {
onSubmit (e) {
e.preventDefault()
this.form.validateFields((error, values) => {
if (!error) {
console.log('ok', values)
} else {
console.log('error', error, values)
}
})
},
changeUseInput (e) {
this.setState({
useInput: e.target.checked,
})
},
},
render () {
const { getFieldError, getFieldDecorator } = this.form
return (
<form onSubmit={this.onSubmit}>
<h2>situation 1</h2>
{this.useInput ? getFieldDecorator('name', {
initialValue: '',
rules: [{
required: true,
message: 'What\'s your name 1?',
}],
})(<input/>) : null}
<span>text content</span>
{this.useInput ? null : getFieldDecorator('name', {
initialValue: '',
rules: [{
required: true,
message: 'What\'s your name 2?',
}],
})(<input />)}
<div>
<label>
<input type='checkbox' checked={this.useInput} onInput={this.changeUseInput} />
change input
</label>
{(getFieldError('name') || []).join(', ')}
</div>
<button>Submit</button>
</form>
)
},
}
const Form2 = {
mixins: [BaseMixin],
props: {
form: Object,
},
data () {
return {
useInput: true,
}
},
beforeMount () {
const { getFieldDecorator } = this.form
this.nameDecorator = getFieldDecorator('name', {
initialValue: '',
rules: [{
required: true,
message: 'What\'s your name?',
}],
})
},
methods: {
onSubmit (e) {
e.preventDefault()
this.form.validateFields((error, values) => {
if (!error) {
console.log('ok', values)
} else {
console.log('error', error, values)
}
})
},
changeUseInput (e) {
this.setState({
useInput: e.target.checked,
})
},
},
render () {
const { getFieldError } = this.form
return (
<form onSubmit={this.onSubmit}>
<h2>situation 2</h2>
{this.useInput ? this.nameDecorator(<input />) : null}
<span>text content</span>
{this.useInput ? null : this.nameDecorator(<input />)}
<div>
<label>
<input type='checkbox' checked={this.useInput} onInput={this.changeUseInput} />
change input
</label>
{(getFieldError('name') || []).join(', ')}
</div>
<button>Submit</button>
</form>
)
},
}
const Form3 = {
mixins: [BaseMixin],
props: {
form: Object,
},
data () {
return {
useInput: false,
}
},
methods: {
onSubmit (e) {
e.preventDefault()
this.form.validateFields((error, values) => {
if (!error) {
console.log('ok', values)
} else {
console.log('error', error, values)
}
})
},
changeUseInput (e) {
this.setState({
useInput: e.target.checked,
})
},
},
render () {
const { getFieldError, getFieldDecorator } = this.form
return (
<form onSubmit={this.onSubmit}>
<h2>situation 3</h2>
{getFieldDecorator('name', {
initialValue: '',
rules: [{
required: true,
message: 'What\'s your name 1?',
}],
})(<input />)}
{this.useInput ? null : getFieldDecorator('name2', {
initialValue: '',
rules: [{
required: true,
message: 'What\'s your name 2?',
}],
})(<input />)}
<div>
<label>
<input type='checkbox' checked={this.useInput} onInput={this.changeUseInput} />
Hide second input
</label>
{(getFieldError('name') || []).join(', ')}
</div>
<button>Submit</button>
</form>
)
},
}
const WrappedForm1 = createForm()(Form1)
const WrappedForm2 = createForm()(Form2)
const WrappedForm3 = createForm()(Form3)
export default {
render () {
return (
<div>
<WrappedForm1 />
<WrappedForm2 />
<WrappedForm3 />
</div>
)
},
}

View File

@ -0,0 +1,131 @@
/* eslint react/no-multi-comp:0, no-console:0 */
import { createForm } from '../index'
import { regionStyle, errorStyle } from './styles'
const Email = {
props: {
form: Object,
hidden: Boolean,
},
render () {
const { hidden, form } = this
const { getFieldProps, getFieldError, isFieldValidating } = form
const errors = getFieldError('email')
const style = {
...regionStyle,
display: hidden ? 'none' : '',
}
return (<div style={ style }>
<div>email:
<input {...getFieldProps('email', {
rules: [
{
required: true,
},
{
type: 'email',
message: '错误的 email 格式',
},
],
hidden,
})}
/></div>
{errors ? <div style={errorStyle}>{errors.join(',')}</div> : null}
{isFieldValidating('email') ? <div style={errorStyle}>validating</div> : null}
</div>)
},
}
const User = {
props: {
form: Object,
},
render () {
const { getFieldProps, getFieldError, isFieldValidating } = this.form
const errors = getFieldError('user')
return (<div style={ regionStyle }>
<div>user:
<input {...getFieldProps('user', {
initialValue: 'x',
rules: [
{
required: true,
},
],
})}
/>
</div>
{errors ? <div style={errorStyle}>{errors.join(',')}</div> : null}
{isFieldValidating('user') ? <div style={errorStyle}>validating</div> : null}
</div>)
},
}
const Form = {
props: {
form: Object,
},
methods: {
onSubmit (e) {
e.preventDefault()
this.form.validateFields((error, values) => {
if (!error) {
console.log('ok', values)
} else {
console.log('error', error, values)
}
})
},
},
render () {
const { form } = this
const { getFieldProps, getFieldValue } = form
return (<div style={{ margin: 20 }}>
<h2>overview</h2>
<form onSubmit={this.onSubmit}>
<div style={ regionStyle }>
<div>
<label>remove/add user:
<input
type='checkbox'
{...getFieldProps('remove_user', {
// initialValue:true,
valuePropName: 'checked',
})}
/>
</label>
</div>
</div>
{ getFieldValue('remove_user') ? null : <User form={ form }/>}
<div style={ regionStyle }>
<div>
<label>hide/show email:
<input
type='checkbox'
{...getFieldProps('hide_email', {
// initialValue:true,
valuePropName: 'checked',
})}
/></label>
</div>
</div>
<Email form={ form } hidden={!!getFieldValue('hide_email')}/>
<div style={ regionStyle }>
<button>submit</button>
</div>
</form>
</div>)
},
}
export default createForm()(Form)

View File

@ -0,0 +1,83 @@
/* eslint react/no-multi-comp:0, no-console:0 */
import { createForm } from '../index'
import { regionStyle, errorStyle } from './styles'
function getFileValueProps (value) {
if (value && value.target) {
return {
value: value.target.value,
}
}
return {
value,
}
}
function getValueFromFileEvent ({ target }) {
return {
target,
}
}
const Form = {
props: {
form: Object,
},
methods: {
onSubmit (e) {
e.preventDefault()
this.form.validateFields((error, values) => {
console.log(error, values)
if (!error) {
console.log('校验通过')
}
})
},
checkSize (rule, value, callback) {
if (value && value.target) {
const files = value.target.files
if (files[0]) {
callback(files[0].size > 1000000 ? 'file size must be less than 1M' : undefined)
} else {
callback()
}
} else {
callback()
}
},
},
render () {
const { getFieldProps, getFieldError } = this.form
const errors = getFieldError('attachment')
return (<div
style={ regionStyle }
>
<div>attachment:</div>
<div>
<input type='file' {...getFieldProps('attachment', {
getValueProps: getFileValueProps,
getValueFromEvent: getValueFromFileEvent,
rules: [this.checkSize],
})}
/>
</div>
<div style={errorStyle}>
{(errors) ? errors.join(',') : null}
</div>
<button onClick={this.onSubmit}>submit</button>
</div>)
},
}
const NewForm = createForm()(Form)
export default {
render () {
return (<div>
<h2>input[type="file"]</h2>
<NewForm />
</div>)
},
}

View File

@ -0,0 +1,55 @@
/* eslint react/no-multi-comp:0, no-console:0 */
import { createForm } from '../index'
const Form = {
props: {
form: Object,
},
beforeMount () {
this.nameDecorator = this.form.getFieldDecorator('name', {
initialValue: '',
rules: [{
required: true,
message: 'What\'s your name?',
}],
})
},
methods: {
onSubmit (e) {
e.preventDefault()
this.form.validateFields((error, values) => {
if (!error) {
console.log('ok', values)
} else {
console.log('error', error, values)
}
})
},
onChange (e) {
console.log(e.target.value)
},
},
render () {
const { getFieldError } = this.form
return (
<form onSubmit={this.onSubmit}>
{this.nameDecorator(
<input
onInput={this.onChange}
/>
)}
<div style={{ color: 'red' }}>
{(getFieldError('name') || []).join(', ')}
</div>
<button>Submit</button>
</form>
)
},
}
export default createForm()(Form)

View File

@ -0,0 +1,66 @@
/* eslint no-console:0 */
import { createForm } from '../index'
import { regionStyle } from './styles'
let uuid = 0
const Form = {
props: {
form: Object,
},
methods: {
remove (k) {
const { form } = this
// can use data-binding to get
let keys = form.getFieldValue('keys')
keys = keys.filter((key) => {
return key !== k
})
// can use data-binding to set
form.setFieldsValue({
keys,
})
},
add () {
uuid++
const { form } = this
// can use data-binding to get
let keys = form.getFieldValue('keys')
keys = keys.concat(uuid)
// can use data-binding to set
// important! notify form to detect changes
form.setFieldsValue({
keys,
})
},
submit (e) {
e.preventDefault()
console.log(this.form.getFieldsValue())
},
},
render () {
const { getFieldProps, getFieldValue } = this.form
getFieldProps('keys', {
initialValue: [],
})
const inputs = getFieldValue('keys').map((k) => {
return (<div key={k} style={ regionStyle }>
<input {...getFieldProps(`name${k}`)}/>
<a
onClick={this.remove.bind(this, k)}
>delete</a></div>)
})
return (<div>
{inputs}
<div style={ regionStyle }>
<button onClick={this.submit}>submit</button>
<button onClick={this.add}>add</button>
</div>
</div>)
},
}
export default createForm()(Form)

View File

@ -0,0 +1,84 @@
/* eslint react/no-multi-comp:0, no-console:0 */
import BaseMixin from '../../_util/BaseMixin'
import createDOMForm from '../src/createDOMForm'
import { Modal } from 'antd'
import { regionStyle, errorStyle } from './styles'
const Form = {
mixins: [BaseMixin],
props: {
form: Object,
},
data () {
return {
visible: false,
}
},
methods: {
onSubmit (e) {
e.preventDefault()
this.form.validateFieldsAndScroll((error, values) => {
if (!error) {
console.log('ok', values)
} else {
console.log('error', error, values)
}
})
},
onCancel () {
this.setState({
visible: false,
})
},
open () {
this.setState({
visible: true,
})
},
},
render () {
const { getFieldProps, getFieldError } = this.form
return (<div style={{ margin: '20px' }}>
<h2>modal</h2>
<Modal
visible={this.visible}
bodyStyle={{
height: '200px',
overflow: 'auto',
}}
onCancel={this.onCancel}
title='modal'
>
<div ref='dialogContent'>
<form onSubmit={this.onSubmit}>
<input
{...getFieldProps('required', {
rules: [{
required: true,
message: '必填',
}],
})}
/>
<div style={errorStyle}>
{getFieldError('required') ? getFieldError('required').join(',')
: <b style={{ visibility: 'hidden' }}>1</b>}
</div>
<div style={{ marginTop: '300px' }}>
<button>submit</button>
</div>
</form>
</div>
</Modal>
<div style={ regionStyle }>
<button onClick={this.open}>open</button>
</div>
</div>)
},
}
export default createDOMForm()(Form)

View File

@ -0,0 +1,194 @@
/* eslint react/no-multi-comp:0, no-console:0 */
import createForm from '../src/createDOMForm'
const Form = {
props: {
form: Object,
},
methods: {
onSubmit (e) {
e.preventDefault()
console.log('Values of member[0].name.firstname and a[0][1].b.c[0]')
console.log(this.form.getFieldsValue(['member[0].name.firstname', 'a[0][1].b.c[0]']))
console.log('Values of all fields')
console.log(this.form.getFieldsValue())
this.form.validateFieldsAndScroll((error, values) => {
if (!error) {
console.log('ok', values)
} else {
console.log('error', error, values)
}
})
},
onChange (e) {
console.log(e.target.value)
},
setField () {
this.form.setFieldsValue({
member: [
{
name: {
firstname: 'm1 first',
lastname: 'm1 last',
},
},
{
name: {
firstname: 'm2 first',
lastname: 'm2 last',
},
},
],
a: [
[undefined, {
b: {
c: ['Value of a[0][1].b.c[0]'],
},
}],
],
w: {
x: {
y: {
z: ['Value of w.x.y.z[0]'],
},
},
},
})
},
resetFields () {
console.log('reset')
this.form.resetFields()
},
},
render () {
const { getFieldDecorator, getFieldError } = this.form
return (
<form onSubmit={this.onSubmit}>
<div>Member 0 firstname</div>
{getFieldDecorator('member[0].name.firstname', {
initialValue: '',
rules: [{
required: true,
message: 'What\'s the member_0 firstname?',
}],
})(
<input
onInput={this.onChange}
/>
)}
<div style={{ color: 'red' }}>
{(getFieldError('member[0].name.firstname') || []).join(', ')}
</div>
<div>Member 0 lastname</div>
{getFieldDecorator('member[0].name.lastname', {
initialValue: '',
rules: [{
required: true,
message: 'What\'s the member_0 lastname?',
}],
})(
<input
onInput={this.onChange}
/>
)}
<div style={{ color: 'red' }}>
{(getFieldError('member[0].name.firstname') || []).join(', ')}
</div>
<div>Member 1 firstname</div>
{getFieldDecorator('member[1].name.firstname', {
initialValue: '',
rules: [{
required: true,
message: 'What\'s the member_1 fistname?',
}],
})(
<input
onInput={this.onChange}
/>
)}
<div style={{ color: 'red' }}>
{(getFieldError('member[1].name.firstname') || []).join(', ')}
</div>
<div>Member 1 lastname</div>
{getFieldDecorator('member[1].name.lastname', {
initialValue: '',
rules: [{
required: true,
message: 'What\'s the member_1 lastname?',
}],
})(
<input
onInput={this.onChange}
/>
)}
<div style={{ color: 'red' }}>
{(getFieldError('member[1].name.firstname') || []).join(', ')}
</div>
<div>a[0][1].b.c[0]</div>
{getFieldDecorator('a[0][1].b.c[0]', {
initialValue: '',
rules: [{
required: true,
message: 'What\'s a[0][1].b.c[0]?',
}],
})(
<input
onInput={this.onChange}
/>
)}
<div style={{ color: 'red' }}>
{(getFieldError('a[0][1].b.c[0]') || []).join(', ')}
</div>
<div>w.x.y.z[0]</div>
{getFieldDecorator('w.x.y.z[0]', {
initialValue: '',
rules: [{
required: true,
message: 'What\'s w.x.y.z[0]?',
}],
})(
<input
onInput={this.onChange}
/>
)}
<div style={{ color: 'red' }}>
{(getFieldError('w.x.y.z[0]') || []).join(', ')}
</div>
<button onClick={this.setField}>Set field</button>
<button onClick={this.resetFields}>Reset fields</button>
<button>Submit</button>
</form>
)
},
}
const NewForm = createForm({
onFieldsChange (_, changedFields, allFields) {
console.log('onFieldsChange: ', changedFields, allFields)
},
onValuesChange (_, changedValues, allValues) {
console.log('onValuesChange: ', changedValues, allValues)
},
})(Form)
export default {
render () {
return (<div>
<h2>setFieldsValue</h2>
<NewForm />
</div>)
},
}

View File

@ -0,0 +1,151 @@
/* eslint react/no-multi-comp:0, no-console:0 */
import { createForm } from '../index'
import { regionStyle, errorStyle } from './styles'
const CustomInput = {
props: {
form: Object,
},
data () {
return {
data: [],
}
},
methods: {
checkUpper (rule, value = '', callback) {
if (value !== value.toUpperCase()) {
callback(new Error('need to be upper!'))
} else {
callback()
}
},
toUpper (v, prev) {
if (v === prev) {
return v
}
return v.toUpperCase()
},
},
render () {
const { getFieldProps, getFieldError } = this.form
const errors = getFieldError('upper')
return (<div style={ regionStyle }>
<div>upper normalize</div>
<div>
<input {...getFieldProps('upper', {
normalize: this.toUpper,
rules: [{
validator: this.checkUpper,
}],
})}
/>
</div>
<div style={errorStyle}>
{(errors) ? errors.join(',') : null}
</div>
</div>)
},
}
const MaxMin = {
props: {
form: Object,
},
methods: {
normalizeMin (value, prevValue, allValues) {
console.log('normalizeMin', allValues.min, allValues.max)
const previousAllValues = this.form.getFieldsValue()
if (allValues.max !== previousAllValues.max) {
// max changed
if (value === '' || Number(allValues.max) < Number(value)) {
return allValues.max
}
}
return value
},
normalizeMax (value, prevValue, allValues) {
console.log('normalizeMax', allValues.min, allValues.max)
const previousAllValues = this.form.getFieldsValue()
if (allValues.min !== previousAllValues.min) {
// min changed
if (value === '' || Number(allValues.min) > Number(value)) {
return allValues.min
}
}
return value
},
},
render () {
const { getFieldProps } = this.form
return (<div style={ regionStyle }>
<div>min: <select
{...getFieldProps('min', {
normalize: this.normalizeMin,
initialValue: '',
})}
>
<option value=''>empty</option>
<option value='1'>1</option>
<option value='2'>2</option>
<option value='3'>3</option>
<option value='4'>4</option>
<option value='5'>5</option>
</select>
</div>
<div>max: <select
{...getFieldProps('max', {
initialValue: '',
normalize: this.normalizeMax,
})}
>
<option value=''>empty</option>
<option value='1'>1</option>
<option value='2'>2</option>
<option value='3'>3</option>
<option value='4'>4</option>
<option value='5'>5</option>
</select>
</div>
</div>)
},
}
const Form = {
// props: {
// form: Object,
// },
methods: {
onSubmit (e) {
e.preventDefault()
this.form.validateFields((error, values) => {
if (!error) {
console.log('ok', values)
} else {
console.log('error', error, values)
}
})
},
},
render () {
const { form } = this
return (<div style={{ margin: '20px' }}>
<h2>normalize</h2>
<form onSubmit={this.onSubmit}>
<CustomInput form={ form }/>
<MaxMin form={ form }/>
<div style={ regionStyle }>
<button>submit</button>
</div>
</form>
</div>)
},
}
export default createForm()(Form)

View File

@ -0,0 +1,259 @@
/* eslint react/no-multi-comp:0, no-console:0 */
import createDOMForm from '../src/createDOMForm'
import { DatePicker, Select } from 'antd'
import { regionStyle, errorStyle } from './styles'
const { Option } = Select
const Email = {
props: {
form: Object,
},
render () {
const { getFieldProps, getFieldError, isFieldValidating } = this.form
const errors = getFieldError('email')
return (<div style={ regionStyle }>
<div>email sync validate</div>
<div>
<input {...getFieldProps('email', {
initialValue: '',
rules: [
{
type: 'email',
message: <b key='err'>错误的 email 格式</b>,
},
],
})}
/></div>
<div style={errorStyle}>
{errors}
</div>
<div style={errorStyle}>
{isFieldValidating('email') ? 'validating' : null}
</div>
</div>)
},
}
const User = {
props: {
form: Object,
},
methods: {
userExists (rule, value, callback) {
setTimeout(() => {
if (value === '1') {
callback([new Error('are you kidding?')])
} else if (value === 'yiminghe') {
callback([new Error('forbid yiminghe')])
} else {
callback()
}
}, 300)
},
},
render () {
const { getFieldProps, getFieldError, isFieldValidating } = this.form
const errors = getFieldError('user')
return (<div style={ regionStyle }>
<div><span style={{ color: 'red' }}>*</span> user async validate</div>
<div>
<input {...getFieldProps('user', {
initialValue: '',
validateFirst: true,
rules: [
{
required: true,
},
{
validator: this.userExists,
},
],
})}
/></div>
<div style={errorStyle}>
{(errors) ? errors.join(',') : null}
</div>
<div style={errorStyle}>
{isFieldValidating('user') ? 'validating' : null}
</div>
</div>)
},
}
const CustomInput = {
props: {
form: Object,
},
render () {
const { getFieldProps, getFieldError, isFieldValidating } = this.form
const errors = getFieldError('select')
return (<div style={ regionStyle }>
<div><span style={{ color: 'red' }}>*</span> custom select sync validate</div>
<div><Select
placeholder='please select'
style={{ width: '200px' }}
{...getFieldProps('select', {
rules: [
{
required: true,
},
],
})}
>
<Option value='1'>1</Option>
<Option value='2'>2</Option>
</Select></div>
<div style={errorStyle}>
{(errors) ? errors.join(',') : null}
</div>
<div style={errorStyle}>
{isFieldValidating('select') ? 'validating' : null}
</div>
</div>)
},
}
const DateInput = {
props: {
form: Object,
},
render () {
const { getFieldProps, getFieldError } = this.form
const errors = getFieldError('date')
return (<div style={ regionStyle }>
<div><span style={{ color: 'red' }}>*</span> DateInput sync validate</div>
<div style={{ width: '200px' }}>
<DatePicker
placeholder='please select'
{...getFieldProps('date', {
rules: [
{
required: true,
type: 'object',
},
],
})}
/>
</div>
<div style={errorStyle}>
{(errors) ? errors.join(',') : null}
</div>
</div>)
},
}
function toNumber (v) {
if (v === undefined) {
return v
}
if (v === '') {
return undefined
}
if (v && v.trim() === '') {
return NaN
}
return Number(v)
}
const NumberInput = {
props: {
form: Object,
},
render () {
const { getFieldProps, getFieldError } = this.form
const errors = getFieldError('number')
return (<div style={ regionStyle }>
<div>number input</div>
<div>
<input
{...getFieldProps('number', {
initialValue: '1',
rules: [{
transform: toNumber,
type: 'number',
}],
})}
/>
</div>
<div style={errorStyle}>
{(errors) ? errors.join(',') : null}
</div>
</div>)
},
}
const Form = {
methods: {
onSubmit (e) {
console.log('submit')
e.preventDefault()
this.form.validateFieldsAndScroll({ scroll: { offsetTop: 20 }}, (error, values) => {
if (!error) {
console.log('ok', values)
} else {
console.log('error', error, values)
}
})
},
reset (e) {
e.preventDefault()
this.form.resetFields()
},
},
render () {
const { form } = this
const { getFieldProps, getFieldError } = form
return (<div style={{ margin: '20px' }}>
<h2>overview</h2>
<form onSubmit={this.onSubmit}>
<User form={ form } saveRef={this.saveRef}/>
<NumberInput form={ form }/>
<Email form={ form }/>
<CustomInput form={ form }/>
<DateInput form={ form }/>
<div style={ regionStyle }>
<div>normal required input</div>
<div>
<input
{...getFieldProps('normal', {
initialValue: '',
rules: [{
required: true,
}],
})}
/>
</div>
<div style={errorStyle}>
{(getFieldError('normal')) ? getFieldError('normal').join(',') : null}
</div>
</div>
<div style={ regionStyle }>
<button onClick={this.reset}>reset</button>
&nbsp;
<input type='submit' value='submit'/>
</div>
</form>
</div>)
},
}
export default createDOMForm({
validateMessages: {
required (field) {
return `${field} 必填`
},
},
})(Form)

View File

@ -0,0 +1,85 @@
/* eslint react/no-multi-comp:0, no-console:0 */
import { createForm } from '../index'
import { regionStyle } from './styles'
import { Switch } from 'antd'
const TopForm = {
props: {
form: Object,
},
render () {
const { getFieldProps } = this.form
return (<div style={ regionStyle }>
<div>has email? </div>
<div>
<Switch {...getFieldProps('on', {
initialValue: true,
valuePropName: 'checked',
})}
/>
</div>
</div>)
},
}
const BottomForm = {
props: {
form: Object,
on: Boolean,
},
render () {
const { form } = this
const on = form.getFieldValue('on')
const style = {
...regionStyle,
display: on ? 'block' : 'none',
}
return (<div style={ style }>
<div>email: </div>
<div>
<input {...form.getFieldProps('email', {
rules: [{
type: 'email',
}],
hidden: !on,
})}
/>
</div>
</div>)
},
}
const Form = {
props: {
form: Object,
},
methods: {
onSubmit (e) {
e.preventDefault()
console.log(this.form.getFieldsValue())
},
},
render () {
const { form } = this
return (<div>
<TopForm form={ form }/>
<BottomForm form={ form }/>
<div style={ regionStyle }>
<button onClick={this.onSubmit}>submit</button>
</div>
</div>)
},
}
const NewForm = createForm()(Form)
export default {
render () {
return (<div>
<h2>parallel form</h2>
<NewForm />
</div>)
},
}

View File

@ -0,0 +1,91 @@
/* eslint no-console:0 */
import { DatePicker } from 'antd'
import createDOMForm from '../src/createDOMForm'
import { regionStyle, errorStyle } from './styles'
const Form = {
props: {
form: Object,
},
methods: {
onSubmit (e) {
console.log('submit')
e.preventDefault()
this.form.validateFieldsAndScroll((error, values) => {
if (!error) {
console.log('ok', values)
} else {
console.log('error', error, values)
}
})
},
reset (e) {
e.preventDefault()
this.form.resetFields()
},
checkStart (rule, value, callback) {
const { validateFields } = this.form
validateFields(['end'], {
force: true,
})
callback()
},
checkEnd (rule, value, callback) {
const end = value
const { getFieldValue } = this.form
const start = getFieldValue('start')
if (!end || !start) {
callback('please select both start and end time')
} else if (end.valueOf() < start.valueOf()) {
callback('start time should be less than end time')
} else {
callback()
}
},
},
render () {
const { form } = this
const { getFieldProps, getFieldError } = form
return (<div style={{ margin: 20 }}>
<h2>startTime and endTime validation</h2>
<form onSubmit={this.onSubmit}>
<div style={ regionStyle }>
<div>start: </div>
<div>
<DatePicker {...getFieldProps('start', {
rules: [this.checkStart],
})}
/>
</div>
</div>
<div style={ regionStyle }>
<div>end: </div>
<div>
<DatePicker {...getFieldProps('end', {
rules: [this.checkEnd],
})}
/>
</div>
</div>
<div style={errorStyle}>
{getFieldError('end') ? getFieldError('end').join(',') : ''}
</div>
<div style={ regionStyle }>
<button onClick={this.reset}>reset</button>
&nbsp;
<input type='submit' value='submit'/>
</div>
</form>
</div>)
},
}
export default createDOMForm()(Form)

View File

@ -0,0 +1,11 @@
export const regionStyle = {
border: '1px solid red',
marginTop: '10px',
padding: '10px',
}
export const errorStyle = {
color: 'red',
marginTop: '10px',
padding: '10px',
}

View File

@ -0,0 +1,120 @@
/* eslint react/no-multi-comp:0, no-console:0 */
import { createForm } from '../index'
import { Select } from 'antd'
import { regionStyle, errorStyle } from './styles'
import { mergeProps } from '../../_util/props-util'
const emailTpl = ['@gmail.com', '@outlook.com', '@qq.com']
const { Option } = Select
const CustomInput = {
props: {
form: Object,
},
data () {
return {
data: [],
}
},
methods: {
onChange (v) {
if (v.indexOf('@') === -1) {
this.data = emailTpl.map(m => v + m)
} else if (this.data.length) {
this.data = []
}
},
},
render () {
const { getFieldProps, getFieldError, isFieldValidating } = this.form
const errors = getFieldError('select')
return (<div style={ regionStyle }>
<div>custom select sync validate</div>
<div>
<Select
{
...mergeProps(
{
props: {
placeholder: 'please select',
mode: 'combobox',
filterOption: false,
},
style: {
width: '200px',
},
}, getFieldProps('select', {
change: this.onChange,
rules: [
{
type: 'email',
},
{
required: true,
},
],
}))
}
>
{this.data.map((d) => {
return <Option key={d} value={d}>{d}</Option>
})}
</Select>
</div>
<div style={errorStyle}>
{(errors) ? errors.join(',')
: <b
style={{
visibility: 'hidden',
}}
>
1
</b>}
</div>
<div style={errorStyle}>
{isFieldValidating('select') ? 'validating' : <b
style={{
visibility: 'hidden',
}}
>
1
</b>}
</div>
</div>)
},
}
const Form = {
props: {
form: Object,
},
methods: {
onSubmit (e) {
e.preventDefault()
this.form.validateFields((error, values) => {
if (!error) {
console.log('ok', values)
} else {
console.log('error', error, values)
}
})
},
},
render () {
const { form } = this
return (<div style={{ margin: '20px' }}>
<h2>suggest</h2>
<form onSubmit={this.onSubmit}>
<CustomInput form={ form }/>
<div style={ regionStyle }>
<button>submit</button>
</div>
</form>
</div>)
},
}
export default createForm()(Form)

View File

@ -0,0 +1,132 @@
/* eslint react/no-multi-comp:0, no-console:0 */
import { createForm } from '../index'
import { regionStyle, errorStyle } from './styles'
const Email = {
props: {
form: Object,
},
render () {
const { getFieldProps, getFieldError, isFieldValidating } = this.form
const errors = getFieldError('email')
return (<div style={ regionStyle }>
<div>email sync validate</div>
<div>
<input {...getFieldProps('email', {
initialValue: '',
validateFirst: true,
rules: [
{
required: true,
},
{
type: 'email',
message: '错误的 email 格式',
},
],
})}
/>
</div>
<div style={errorStyle}>
{errors ? errors.join(',') : null}
</div>
<div style={errorStyle}>
{isFieldValidating('email') ? 'validating' : null}
</div>
</div>)
},
}
const User = {
props: {
form: Object,
},
methods: {
userExists (rule, value, callback) {
setTimeout(() => {
if (value === '1') {
callback([new Error('are you kidding?')])
} else if (value === 'yiminghe') {
callback([new Error('forbid yiminghe')])
} else {
callback()
}
}, 300)
},
},
render () {
const { getFieldProps, getFieldError, isFieldValidating } = this.form
const errors = getFieldError('user')
return (<div style={ regionStyle }>
<div>user async validate</div>
<div>
<input {...getFieldProps('user', {
initialValue: '',
rules: [
{
required: true,
min: 2,
},
{
validator: this.userExists,
},
],
})}
/>
</div>
<div style={errorStyle}>
{(errors) ? errors.join(',') : null}
</div>
<div style={errorStyle}>
{isFieldValidating('user') ? 'validating' : null}
</div>
</div>)
},
}
const Form = {
methods: {
onSubmit (e) {
console.log('submit')
e.preventDefault()
this.form.validateFields({
// firstFields: false,
}, (error, values) => {
if (!error) {
console.log('ok', values)
} else {
console.log('error', error, values)
}
})
},
reset (e) {
e.preventDefault()
this.form.resetFields()
},
},
render () {
const { form } = this
return (<div style={{ margin: '20px' }}>
<h2>validateFirst</h2>
<form onSubmit={this.onSubmit}>
<User form={ form }/>
<Email form={ form }/>
<div style={ regionStyle }>
<button onClick={this.reset}>reset</button>
&nbsp;
<input type='submit' value='submit'/>
</div>
</form>
</div>)
},
}
export default createForm()(Form)

View File

@ -0,0 +1,112 @@
/* eslint react/no-multi-comp:0, no-console:0 */
import { createForm } from '../index'
import { regionStyle, errorStyle } from './styles'
const Email = {
props: {
form: Object,
},
render () {
const { getFieldProps, getFieldError, isFieldValidating } = this.form
const errors = getFieldError('email')
return (<div style={ regionStyle }>
<div>email validate onBlur && onChange</div>
<div>
<input {...getFieldProps('email', {
validate: [{
trigger: 'blur',
rules: [{
required: true,
}],
}, {
trigger: ['blur', 'input'],
rules: [{
type: 'email',
message: '错误的 email 格式',
}],
}],
})}
/>
</div>
<div style={errorStyle}>
{errors ? errors.join(',') : null}
</div>
<div style={errorStyle}>
{isFieldValidating('email') ? 'validating' : null}
</div>
</div>)
},
}
const User = {
props: {
form: Object,
},
render () {
const { getFieldProps, getFieldError, isFieldValidating } = this.form
const errors = getFieldError('user')
return (<div style={ regionStyle }>
<div>user validate on submit</div>
<div>
<input {...getFieldProps('user', {
rules: [
{
required: true,
},
{
type: 'string',
min: 5,
},
],
validateTrigger: null,
})}
/>
</div>
<div style={errorStyle}>
{(errors) ? errors.join(',') : null}
</div>
<div style={errorStyle}>
{isFieldValidating('user') ? 'validating' : null}
</div>
</div>)
},
}
const Form = {
props: {
form: Object,
},
methods: {
onSubmit (e) {
e.preventDefault()
this.form.validateFields((error, values) => {
if (!error) {
console.log('ok', values)
} else {
console.log('error', error, values)
}
})
},
},
render () {
const { form } = this
return (<div style={{ margin: '20px' }}>
<h2>use validateTrigger config</h2>
<form onSubmit={this.onSubmit}>
<User form={ form }/>
<Email form={ form }/>
<div style={ regionStyle }>
<button>submit</button>
</div>
</form>
</div>)
},
}
export default createForm()(Form)

View File

@ -0,0 +1,3 @@
// export this package's api
import { createForm, createFormField } from './src/'
export { createForm, createFormField }

View File

@ -0,0 +1,589 @@
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',
props = {},
} = 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.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,
}
},
watch: {
'$props': {
handler: function (nextProps) {
if (mapPropsToFields) {
this.fieldsStore.updateFields(mapPropsToFields(nextProps))
}
},
deep: true,
},
},
mounted () {
this.wrappedComponentRef(this.$refs.WrappedComponent)
},
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]))
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] = fn.bind(this, name, action)
}
return cache[action]
},
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]
}
},
getFieldDecorator (name, fieldOption) {
const { props, ...restProps } = this.getFieldProps(name, fieldOption)
return (fieldElem) => {
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}\` 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] = 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)
}
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())
}
this.$forceUpdate()
this.$nextTick(() => {
callback && callback()
})
},
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 = {}
}
},
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) {
// 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
}
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
},
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 { 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))
}
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)
},
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 props = getOptionProps(this)
const wrappedComponentProps = {
props: mapProps.call(this, {
...formProps,
...props,
}),
on: $listeners,
ref: 'WrappedComponent',
}
return <WrappedComponent {...wrappedComponentProps}>{$slots.default}</WrappedComponent>
},
}
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

View File

@ -0,0 +1,107 @@
import scrollIntoView from 'dom-scroll-into-view'
import has from 'lodash/has'
import createBaseForm from './createBaseForm'
import { mixin as formMixin } from './createForm'
import { getParams } from './utils'
function computedStyle (el, prop) {
const getComputedStyle = window.getComputedStyle
const style =
// If we have getComputedStyle
getComputedStyle
// Query it
// TODO: From CSS-Query notes, we might need (node, null) for FF
? getComputedStyle(el)
// Otherwise, we are in IE and use currentStyle
: el.currentStyle
if (style) {
return style[
// Switch to camelCase for CSSOM
// DEV: Grabbed from jQuery
// https://github.com/jquery/jquery/blob/1.9-stable/src/css.js#L191-L194
// https://github.com/jquery/jquery/blob/1.9-stable/src/core.js#L593-L597
prop.replace(/-(\w)/gi, (word, letter) => {
return letter.toUpperCase()
})
]
}
return undefined
}
function getScrollableContainer (n) {
let node = n
let nodeName
/* eslint no-cond-assign:0 */
while ((nodeName = node.nodeName.toLowerCase()) !== 'body') {
const overflowY = computedStyle(node, 'overflowY')
// https://stackoverflow.com/a/36900407/3040605
if (
node !== n &&
(overflowY === 'auto' || overflowY === 'scroll') &&
node.scrollHeight > node.clientHeight
) {
return node
}
node = node.parentNode
}
return nodeName === 'body' ? node.ownerDocument : node
}
const mixin = {
methods: {
getForm () {
return {
...formMixin.methods.getForm.call(this),
validateFieldsAndScroll: this.validateFieldsAndScroll,
}
},
validateFieldsAndScroll (ns, opt, cb) {
const { names, callback, options } = getParams(ns, opt, cb)
const newCb = (error, values) => {
if (error) {
const validNames = this.fieldsStore.getValidFieldsName()
let firstNode
let firstTop
for (const name of validNames) {
if (has(error, name)) {
const instance = this.getFieldInstance(name)
if (instance) {
const node = instance.$el || instance.elm
const top = node.getBoundingClientRect().top
if (firstTop === undefined || firstTop > top) {
firstTop = top
firstNode = node
}
}
}
}
if (firstNode) {
const c = options.container || getScrollableContainer(firstNode)
scrollIntoView(firstNode, c, {
onlyScrollIfNeeded: true,
...options.scroll,
})
}
}
if (typeof callback === 'function') {
callback(error, values)
}
}
return this.validateFields(names, options, newCb)
},
},
}
function createDOMForm (option) {
return createBaseForm({
...option,
}, [mixin])
}
export default createDOMForm

View File

@ -0,0 +1,261 @@
import set from 'lodash/set'
import createFormField, { isFormField } from './createFormField'
import {
flattenFields,
getErrorStrs,
startsWith,
} from './utils'
function partOf (a, b) {
return b.indexOf(a) === 0 && ['.', '['].indexOf(b[a.length]) !== -1
}
class FieldsStore {
constructor (fields) {
this.fields = this.flattenFields(fields)
this.fieldsMeta = {}
}
updateFields (fields) {
this.fields = this.flattenFields(fields)
}
flattenFields (fields) {
return flattenFields(
fields,
(_, node) => isFormField(node),
'You must wrap field data with `createFormField`.'
)
}
flattenRegisteredFields (fields) {
const validFieldsName = this.getAllFieldsName()
return flattenFields(
fields,
path => validFieldsName.indexOf(path) >= 0,
'You cannot set field before registering it.'
)
}
setFieldsInitialValue = (initialValues) => {
const flattenedInitialValues = this.flattenRegisteredFields(initialValues)
const fieldsMeta = this.fieldsMeta
Object.keys(flattenedInitialValues).forEach(name => {
if (fieldsMeta[name]) {
this.setFieldMeta(name, {
...this.getFieldMeta(name),
initialValue: flattenedInitialValues[name],
})
}
})
}
setFields (fields) {
const fieldsMeta = this.fieldsMeta
const nowFields = {
...this.fields,
...fields,
}
const nowValues = {}
Object.keys(fieldsMeta)
.forEach((f) => { nowValues[f] = this.getValueFromFields(f, nowFields) })
Object.keys(nowValues).forEach((f) => {
const value = nowValues[f]
const fieldMeta = this.getFieldMeta(f)
if (fieldMeta && fieldMeta.normalize) {
const nowValue =
fieldMeta.normalize(value, this.getValueFromFields(f, this.fields), nowValues)
if (nowValue !== value) {
nowFields[f] = {
...nowFields[f],
value: nowValue,
}
}
}
})
this.fields = nowFields
}
resetFields (ns) {
const { fields } = this
const names = ns
? this.getValidFieldsFullName(ns)
: this.getAllFieldsName()
return names.reduce((acc, name) => {
const field = fields[name]
if (field && 'value' in field) {
acc[name] = {}
}
return acc
}, {})
}
setFieldMeta (name, meta) {
this.fieldsMeta[name] = meta
}
getFieldMeta (name) {
this.fieldsMeta[name] = this.fieldsMeta[name] || {}
return this.fieldsMeta[name]
}
getValueFromFields (name, fields) {
const field = fields[name]
if (field && 'value' in field) {
return field.value
}
const fieldMeta = this.getFieldMeta(name)
return fieldMeta && fieldMeta.initialValue
}
getAllValues = () => {
const { fieldsMeta, fields } = this
return Object.keys(fieldsMeta)
.reduce((acc, name) => set(acc, name, this.getValueFromFields(name, fields)), {})
}
getValidFieldsName () {
const { fieldsMeta } = this
return fieldsMeta
? Object.keys(fieldsMeta).filter(name => !this.getFieldMeta(name).hidden)
: []
}
getAllFieldsName () {
const { fieldsMeta } = this
return fieldsMeta ? Object.keys(fieldsMeta) : []
}
getValidFieldsFullName (maybePartialName) {
const maybePartialNames = Array.isArray(maybePartialName)
? maybePartialName : [maybePartialName]
return this.getValidFieldsName()
.filter(fullName => maybePartialNames.some(partialName => (
fullName === partialName || (
startsWith(fullName, partialName) &&
['.', '['].indexOf(fullName[partialName.length]) >= 0
)
)))
}
getFieldValuePropValue (fieldMeta) {
const { name, getValueProps, valuePropName } = fieldMeta
const field = this.getField(name)
const fieldValue = 'value' in field
? field.value : fieldMeta.initialValue
if (getValueProps) {
return getValueProps(fieldValue)
}
return { [valuePropName]: fieldValue }
}
getField (name) {
return {
...this.fields[name],
name,
}
}
getNotCollectedFields () {
return this.getValidFieldsName()
.filter(name => !this.fields[name])
.map(name => ({
name,
dirty: false,
value: this.getFieldMeta(name).initialValue,
}))
.reduce((acc, field) => set(acc, field.name, createFormField(field)), {})
}
getNestedAllFields () {
return Object.keys(this.fields)
.reduce(
(acc, name) => set(acc, name, createFormField(this.fields[name])),
this.getNotCollectedFields()
)
}
getFieldMember (name, member) {
return this.getField(name)[member]
}
getNestedFields (names, getter) {
const fields = names || this.getValidFieldsName()
return fields.reduce((acc, f) => set(acc, f, getter(f)), {})
}
getNestedField (name, getter) {
const fullNames = this.getValidFieldsFullName(name)
if (
fullNames.length === 0 || // Not registered
(fullNames.length === 1 && fullNames[0] === name) // Name already is full name.
) {
return getter(name)
}
const isArrayValue = fullNames[0][name.length] === '['
const suffixNameStartIndex = isArrayValue ? name.length : name.length + 1
return fullNames
.reduce(
(acc, fullName) => set(
acc,
fullName.slice(suffixNameStartIndex),
getter(fullName)
),
isArrayValue ? [] : {}
)
}
getFieldsValue = (names) => {
return this.getNestedFields(names, this.getFieldValue)
}
getFieldValue = (name) => {
const { fields } = this
return this.getNestedField(name, (fullName) => this.getValueFromFields(fullName, fields))
}
getFieldsError = (names) => {
return this.getNestedFields(names, this.getFieldError)
}
getFieldError = (name) => {
return this.getNestedField(
name,
(fullName) => getErrorStrs(this.getFieldMember(fullName, 'errors'))
)
}
isFieldValidating = (name) => {
return this.getFieldMember(name, 'validating')
}
isFieldsValidating = (ns) => {
const names = ns || this.getValidFieldsName()
return names.some((n) => this.isFieldValidating(n))
}
isFieldTouched = (name) => {
return this.getFieldMember(name, 'touched')
}
isFieldsTouched = (ns) => {
const names = ns || this.getValidFieldsName()
return names.some((n) => this.isFieldTouched(n))
}
// @private
// BG: `a` and `a.b` cannot be use in the same form
isValidNestedFieldName (name) {
const names = this.getAllFieldsName()
return names.every(n => !partOf(n, name) && !partOf(name, n))
}
clearField (name) {
delete this.fields[name]
delete this.fieldsMeta[name]
}
}
export default function createFieldsStore (fields) {
return new FieldsStore(fields)
}

View File

@ -0,0 +1,34 @@
import createBaseForm from './createBaseForm'
export const mixin = {
methods: {
getForm () {
return {
getFieldsValue: this.fieldsStore.getFieldsValue,
getFieldValue: this.fieldsStore.getFieldValue,
getFieldInstance: this.getFieldInstance,
setFieldsValue: this.setFieldsValue,
setFields: this.setFields,
setFieldsInitialValue: this.fieldsStore.setFieldsInitialValue,
getFieldDecorator: this.getFieldDecorator,
getFieldProps: this.getFieldProps,
getFieldsError: this.fieldsStore.getFieldsError,
getFieldError: this.fieldsStore.getFieldError,
isFieldValidating: this.fieldsStore.isFieldValidating,
isFieldsValidating: this.fieldsStore.isFieldsValidating,
isFieldsTouched: this.fieldsStore.isFieldsTouched,
isFieldTouched: this.fieldsStore.isFieldTouched,
isSubmitting: this.isSubmitting,
submit: this.submit,
validateFields: this.validateFields,
resetFields: this.resetFields,
}
},
},
}
function createForm (options) {
return createBaseForm(options, [mixin])
}
export default createForm

View File

@ -0,0 +1,16 @@
class Field {
constructor (fields) {
Object.assign(this, fields)
}
}
export function isFormField (obj) {
return obj instanceof Field
}
export default function createFormField (field) {
if (isFormField(field)) {
return field
}
return new Field(field)
}

View File

@ -0,0 +1,9 @@
// export this package's api
import createForm from './createForm'
import createFormField from './createFormField'
import formShape from './propTypes'
import Vue from 'vue'
import antRefDirective from '../../_util/antRefDirective'
Vue.use(antRefDirective)
export { createForm, createFormField, formShape }

View File

@ -0,0 +1,24 @@
import PropTypes from '../../_util/vue-types'
const formShape = PropTypes.shape({
getFieldsValue: PropTypes.func,
getFieldValue: PropTypes.func,
getFieldInstance: PropTypes.func,
setFieldsValue: PropTypes.func,
setFields: PropTypes.func,
setFieldsInitialValue: PropTypes.func,
getFieldDecorator: PropTypes.func,
getFieldProps: PropTypes.func,
getFieldsError: PropTypes.func,
getFieldError: PropTypes.func,
isFieldValidating: PropTypes.func,
isFieldsValidating: PropTypes.func,
isFieldsTouched: PropTypes.func,
isFieldTouched: PropTypes.func,
isSubmitting: PropTypes.func,
submit: PropTypes.func,
validateFields: PropTypes.func,
resetFields: PropTypes.func,
}).loose
export default formShape

View File

@ -0,0 +1,152 @@
function getDisplayName (WrappedComponent) {
return WrappedComponent.name || 'WrappedComponent'
}
export function argumentContainer (Container, WrappedComponent) {
/* eslint no-param-reassign:0 */
Container.name = `Form_${getDisplayName(WrappedComponent)}`
Container.WrappedComponent = WrappedComponent
Container.props = { ...Container.props, ...WrappedComponent.props }
return Container
}
export function identity (obj) {
return obj
}
export function flattenArray (arr) {
return Array.prototype.concat.apply([], arr)
}
export function treeTraverse (path = '', tree, isLeafNode, errorMessage, callback) {
if (isLeafNode(path, tree)) {
callback(path, tree)
} else if (tree === undefined) {
return
} else if (Array.isArray(tree)) {
tree.forEach((subTree, index) => treeTraverse(
`${path}[${index}]`,
subTree,
isLeafNode,
errorMessage,
callback
))
} else { // It's object and not a leaf node
if (typeof tree !== 'object') {
console.error(errorMessage)
return
}
Object.keys(tree).forEach(subTreeKey => {
const subTree = tree[subTreeKey]
treeTraverse(
`${path}${path ? '.' : ''}${subTreeKey}`,
subTree,
isLeafNode,
errorMessage,
callback
)
})
}
}
export function flattenFields (maybeNestedFields, isLeafNode, errorMessage) {
const fields = {}
treeTraverse(undefined, maybeNestedFields, isLeafNode, errorMessage, (path, node) => {
fields[path] = node
})
return fields
}
export function normalizeValidateRules (validate, rules, validateTrigger) {
const validateRules = validate.map((item) => {
const newItem = {
...item,
trigger: item.trigger || [],
}
if (typeof newItem.trigger === 'string') {
newItem.trigger = [newItem.trigger]
}
return newItem
})
if (rules) {
validateRules.push({
trigger: validateTrigger ? [].concat(validateTrigger) : [],
rules,
})
}
return validateRules
}
export function getValidateTriggers (validateRules) {
return validateRules
.filter(item => !!item.rules && item.rules.length)
.map(item => item.trigger)
.reduce((pre, curr) => pre.concat(curr), [])
}
export function getValueFromEvent (e) {
// To support custom element
if (!e || !e.target) {
return e
}
const { target } = e
return target.type === 'checkbox' ? target.checked : target.value
}
export function getErrorStrs (errors) {
if (errors) {
return errors.map((e) => {
if (e && e.message) {
return e.message
}
return e
})
}
return errors
}
export function getParams (ns, opt, cb) {
let names = ns
let options = opt
let callback = cb
if (cb === undefined) {
if (typeof names === 'function') {
callback = names
options = {}
names = undefined
} else if (Array.isArray(names)) {
if (typeof options === 'function') {
callback = options
options = {}
} else {
options = options || {}
}
} else {
callback = options
options = names || {}
names = undefined
}
}
return {
names,
options,
callback,
}
}
export function isEmptyObject (obj) {
return Object.keys(obj).length === 0
}
export function hasRules (validate) {
if (validate) {
return validate.some((item) => {
return item.rules && item.rules.length
})
}
return false
}
export function startsWith (str, prefix) {
return str.lastIndexOf(prefix, 0) === 0
}

View File

@ -491,8 +491,9 @@ export default {
const inputDisplayValueFormat = this.formatWrapper(inputDisplayValue)
const isUpDisabled = !!upDisabledClass || disabled || readOnly
const isDownDisabled = !!downDisabledClass || disabled || readOnly
const { mouseenter = noop, mouseleave = noop, mouseover = noop, mouseout = noop } = this.$listeners
const contentProps = {
on: this.$listeners,
on: { mouseenter, mouseleave, mouseover, mouseout },
class: classes,
}
const upHandlerProps = {
@ -578,7 +579,7 @@ export default {
step={this.step}
name={this.name}
id={this.id}
onChange={this.onChange}
onInput={this.onChange}
ref='inputRef'
value={inputDisplayValueFormat}
pattern={this.pattern}

View File

@ -7,6 +7,9 @@ import createChainedFunction from '../_util/createChainedFunction'
import getTransitionProps from '../_util/getTransitionProps'
import Notice from './Notice'
import antRefDirective from '../_util/antRefDirective'
Vue.use(antRefDirective)
let seed = 0
const now = Date.now()

View File

@ -6,7 +6,7 @@ import classes from 'component-classes'
import { Item as MenuItem, ItemGroup as MenuItemGroup } from '../vc-menu'
import warning from 'warning'
import Option from './Option'
import { hasProp, getSlotOptions, getPropsData, getValueByProp as getValue, getComponentFromProp, getEvents, getClass } from '../_util/props-util'
import { hasProp, getSlotOptions, getPropsData, getValueByProp as getValue, getComponentFromProp, getEvents, getClass, getStyle, getAttrs } from '../_util/props-util'
import getTransitionProps from '../_util/getTransitionProps'
import { cloneElement } from '../_util/vnode'
import BaseMixin from '../_util/BaseMixin'
@ -45,6 +45,7 @@ function chaining (...fns) {
}
}
export default {
inheritAttrs: false,
name: 'Select',
mixins: [BaseMixin],
props: {
@ -706,9 +707,10 @@ export default {
},
_getInputElement () {
const props = this.$props
const attrs = getAttrs(this)
const inputElement = props.getInputElement
? props.getInputElement()
: <input id={props.id} autoComplete='off'/>
: <input id={attrs.id} autoComplete='off'/>
const inputCls = classnames(getClass(inputElement), {
[`${props.prefixCls}-search__field`]: true,
})
@ -1501,6 +1503,7 @@ export default {
selectionProps.attrs.tabIndex = props.disabled ? -1 : 0
}
const rootCls = {
...getClass(this),
[prefixCls]: true,
[`${prefixCls}-open`]: openStatus,
[`${prefixCls}-focused`]: openStatus || !!this._focused,
@ -1542,6 +1545,7 @@ export default {
>
<div
ref='rootRef'
style={getStyle(this)}
class={classnames(rootCls)}
// tabindex='-1'
// onBlur={this.onOuterBlur}

View File

@ -10,6 +10,7 @@ const upLoadPropTypes = {
// style: PropTypes.object,
prefixCls: PropTypes.string,
action: PropTypes.string,
name: PropTypes.string,
// className: PropTypes.string,
multiple: PropTypes.bool,
disabled: PropTypes.bool,

View File

@ -1,6 +1,6 @@
{
"name": "vue-antd-ui",
"version": "0.4.1",
"version": "0.5.1",
"title": "Ant Design Vue",
"description": "An enterprise-class UI design language and Vue-based implementation",
"keywords": [
@ -63,6 +63,7 @@
"babel-plugin-istanbul": "^4.1.1",
"babel-plugin-syntax-dynamic-import": "^6.18.0",
"babel-plugin-syntax-jsx": "^6.18.0",
"babel-plugin-transform-class-properties": "^6.24.1",
"babel-plugin-transform-decorators": "^6.24.1",
"babel-plugin-transform-decorators-legacy": "^1.3.4",
"babel-plugin-transform-es3-member-expression-literals": "^6.22.0",
@ -128,6 +129,7 @@
"pre-commit": "^1.2.2",
"puppeteer": "^1.3.0",
"querystring": "^0.2.0",
"raw-loader": "^1.0.0-beta.0",
"reqwest": "^2.0.5",
"rimraf": "^2.6.2",
"rucksack-css": "^1.0.2",
@ -154,8 +156,9 @@
"dependencies": {
"add-dom-event-listener": "^1.0.2",
"array-tree-filter": "^2.1.0",
"babel-runtime": "6.x",
"async-validator": "^1.8.2",
"babel-helper-vue-jsx-merge-props": "^2.0.3",
"babel-runtime": "6.x",
"classnames": "^2.2.5",
"component-classes": "^1.2.6",
"css-animation": "^1.4.1",

View File

@ -19,7 +19,7 @@ import {
DatePicker,
Divider,
Dropdown,
// Form,
Form,
Icon,
Input,
InputNumber,
@ -85,7 +85,8 @@ Vue.component(DatePicker.WeekPicker.name, DatePicker.WeekPicker)
Vue.component(Divider.name, Divider)
Vue.component(Dropdown.name, Dropdown)
Vue.component(Dropdown.Button.name, Dropdown.Button)
// Vue.component(Form.name, Form)
Vue.component(Form.name, Form)
Vue.component(Form.Item.name, Form.Item)
Vue.component(Icon.name, Icon)
Vue.component(Input.name, Input)
Vue.component(Input.Group.name, Input.Group)

View File

@ -51,10 +51,10 @@ export default {
},
data () {
const { name = '' } = this.$route.params
const { html, script, style, us, cn } = this.jsfiddle
let sourceCode = `<template>${html}</template>\n`
sourceCode = script ? sourceCode + '\<script>' + script + '<\/script>' : sourceCode
sourceCode = style ? sourceCode + '\<style>' + style + '<\/style>' : sourceCode
const { html, script, style, us, cn, sourceCode } = this.jsfiddle
// let sourceCode = `<template>${html}</template>\n`
// sourceCode = script ? sourceCode + '\<script>' + script + '<\/script>' : sourceCode
// sourceCode = style ? sourceCode + '\<style>' + style + '<\/style>' : sourceCode
const usTitle = (us.split('#### ')[1] || '').split('\n')[0] || ''
const cnTitle = (cn.split('#### ')[1] || '').split('\n')[0] || ''
if (process.env.NODE_ENV !== 'production' && usTitle === '') {

View File

@ -0,0 +1,81 @@
<template>
<div>
<demo-box :jsfiddle="jsfiddle">
<template slot="component"><slot /></template>
<template slot="description">
<div v-html="cnHtml"></div>
</template>
<template slot="us-description">
<div v-html="usHtml"></div>
</template>
<template slot="code">
<div v-html="codeHtml"></div>
</template>
</demo-box>
</div>
</template>
<script>
import marked from 'marked'
const hljs = require('highlight.js')
const replaceDelimiters = function (str) {
return str.replace(/({{|}})/g, '<span>$1</span>')
}
const renderHighlight = function (str, lang) {
if (!(lang && hljs.getLanguage(lang))) {
return ''
}
try {
return replaceDelimiters(hljs.highlight(lang, str, true).value)
} catch (err) {}
}
const renderer = new marked.Renderer()
renderer.heading = function (text, level) {
return '<h' +
level +
' id="' +
text.replace(/[^\w]+/g, '-') +
'">' +
text +
'</h' +
level +
'>\n'
}
marked.setOptions({
renderer,
gfm: true,
tables: true,
breaks: true,
pedantic: true,
sanitize: true,
smartLists: true,
smartypants: true,
html: true,
highlight: renderHighlight,
})
const cnReg = /<cn>([\S\s\t]*?)<\/cn>/
const usReg = /<us>([\S\s\t]*?)<\/us>/
export default {
name: 'demoContainer',
props: ['code'],
data () {
const cn = this.code.match(cnReg) || []
const us = this.code.match(usReg) || []
const cnHtml = marked(cn[1].trim())
const usHtml = marked(us[1].trim())
const sourceCode = this.code.replace(cn[0], '').replace(us[0], '').trim()
const codeHtml = marked('````jsx\n' + sourceCode + '````')
return {
codeHtml,
cnHtml,
usHtml,
jsfiddle: {
sourceCode,
cn: cn[1].trim(),
us: us[1].trim(),
},
}
},
}
</script>

View File

@ -4,18 +4,18 @@
<script>
import marked from 'marked'
import { isZhCN } from '../util'
var renderer = new marked.Renderer();
renderer.heading = function(text, level) {
return '<h'
+ level
+ ' id="'
+ text.replace(/[^\w]+/g, '-')
+ '">'
+ text
+ '</h'
+ level
+ '>\n';
};
const renderer = new marked.Renderer()
renderer.heading = function (text, level) {
return '<h' +
level +
' id="' +
text.replace(/[^\w]+/g, '-') +
'">' +
text +
'</h' +
level +
'>\n'
}
marked.setOptions({
renderer,
gfm: true,

View File

@ -44,3 +44,4 @@ export { default as transfer } from 'antd/transfer/demo/index.vue'
export { default as upload } from 'antd/upload/demo/index.vue'
export { default as tree } from 'antd/tree/demo/index.vue'
export { default as layout } from 'antd/layout/demo/index.vue'
export { default as form } from 'antd/form/demo/index.vue'

View File

@ -1,3 +1,4 @@
import 'babel-polyfill'
import '../components/style.js'
import './index.less'
import 'highlight.js/styles/solarized-light.css'
@ -9,12 +10,14 @@ import Md from './components/md'
import Api from './components/api'
import './components'
import demoBox from './components/demoBox'
import demoContainer from './components/demoContainer'
Vue.use(VueClipboard)
Vue.use(VueRouter)
Vue.component(Md.name, Md)
Vue.component(Api.name, Api)
Vue.component('demo-box', demoBox)
Vue.component('demo-container', demoContainer)
const router = new VueRouter({
mode: 'history',

View File

@ -4,7 +4,7 @@ import Iframe from './components/iframe.vue'
const AsyncTestComp = () => {
const d = window.location.hash.replace('#', '')
return {
component: import(`../components/layout/demo/${d}`),
component: import(`../components/upload/demo/${d}`),
}
}

View File

@ -65,6 +65,7 @@ md.core.ruler.push('update_template', function replace ({ tokens }) {
let style = ''
let scopedStyle = ''
let code = ''
let sourceCode = ''
tokens.forEach(token => {
if (token.type === 'html_block') {
if (token.content.match(cnReg)) {
@ -77,6 +78,7 @@ md.core.ruler.push('update_template', function replace ({ tokens }) {
}
}
if (token.type === 'fence' && token.info === 'html' && token.markup === '```') {
sourceCode = token.content
code = '````html\n' + token.content + '````'
template = fetch(token.content, 'template')
script = fetch(token.content, 'script')
@ -93,6 +95,7 @@ md.core.ruler.push('update_template', function replace ({ tokens }) {
style,
us,
cn,
sourceCode,
}
jsfiddle = md.utils.escapeHtml(JSON.stringify(jsfiddle))
const codeHtml = code ? md.render(code) : ''