feat: add form

This commit is contained in:
tjz 2018-05-06 18:32:40 +08:00
parent 790fe9c4b9
commit 138274f9c6
11 changed files with 901 additions and 71 deletions

View File

@ -97,6 +97,9 @@ export default {
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) {

View File

@ -0,0 +1,82 @@
<cn>
#### 表单联动
使用 `setFieldsValue` 来动态设置其他控件的值。
</cn>
<us>
#### Coordinated Controls
Use `setFieldsValue` to set other control's value programmaticly.
</us>
```html
<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>
```html
<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>
```html
<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>
```html
<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>
```html
<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>
```html
<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>
```html
<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,81 @@
<cn>
#### 表单布局
表单有三种布局。
</cn>
<us>
#### Form Layout
There are three layout for form: `horizontal`, `vertical`, `inline`.
</us>
```html
<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

@ -1,64 +1,67 @@
<script>
import { Form } from 'vue-antd-ui'
<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>
const NormalLoginForm = {
<script>
export default {
data () {
return {
formLayout: 'horizontal',
}
},
methods: {
handleSubmit (e) {
e.preventDefault()
this.form.validateFields((err, values) => {
if (!err) {
console.log('Received values of form: ', values)
}
})
handleFormLayoutChange (e) {
this.formLayout = e.target.value
},
},
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>
)
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 },
} : {}
},
},
}
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

@ -7,7 +7,7 @@ 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 PropTypes from '../../_util/vue-types'
import {
argumentContainer,
@ -34,17 +34,24 @@ function createBaseForm (option = {}, mixins = []) {
fieldMetaProp,
fieldDataProp,
formPropName = 'form',
// @deprecated
withRef,
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: {
// hideRequiredMark: PropTypes.bool,
// layout: PropTypes.string,
// },
props: {
...formProps,
wrappedComponentRef: PropTypes.func.def(() => {}),
},
data () {
const fields = mapPropsToFields && mapPropsToFields(this.$props)
this.fieldsStore = createFieldsStore(fields || {})
@ -81,6 +88,9 @@ function createBaseForm (option = {}, mixins = []) {
deep: true,
},
},
mounted () {
this.wrappedComponentRef(this.$refs.WrappedComponent)
},
methods: {
onCollectCommon (name, action, args) {
const fieldMeta = this.fieldsStore.getFieldMeta(name)
@ -554,17 +564,22 @@ function createBaseForm (option = {}, mixins = []) {
...props,
}),
on: $listeners,
}
if (withRef) {
wrappedComponentProps.ref = 'wrappedComponent'
ref: 'WrappedComponent',
}
return <WrappedComponent {...wrappedComponentProps}/>
},
}
if (!(WrappedComponent.props && formPropName in WrappedComponent.props)) {
WrappedComponent.props = {
...WrappedComponent.props,
[formPropName]: Object,
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)