feat(Form): add required prop and support auto display (#12380)

* feat(Form): add required prop and support auto display

* fix: type

* chore: fix

* chore: shorter
This commit is contained in:
neverland 2023-10-22 08:07:42 +08:00 committed by GitHub
parent 77925bfb16
commit 86688394d2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 403 additions and 35 deletions

View File

@ -38,13 +38,16 @@ export const cellSharedProps = {
center: Boolean, center: Boolean,
isLink: Boolean, isLink: Boolean,
border: truthProp, border: truthProp,
required: Boolean,
iconPrefix: String, iconPrefix: String,
valueClass: unknownProp, valueClass: unknownProp,
labelClass: unknownProp, labelClass: unknownProp,
titleClass: unknownProp, titleClass: unknownProp,
titleStyle: null as unknown as PropType<string | CSSProperties>, titleStyle: null as unknown as PropType<string | CSSProperties>,
arrowDirection: String as PropType<CellArrowDirection>, arrowDirection: String as PropType<CellArrowDirection>,
required: {
type: [Boolean, String] as PropType<boolean | 'auto'>,
default: null,
},
clickable: { clickable: {
type: Boolean as PropType<boolean | null>, type: Boolean as PropType<boolean | null>,
default: null, default: null,
@ -147,7 +150,7 @@ export default defineComponent({
const classes: Record<string, boolean | undefined> = { const classes: Record<string, boolean | undefined> = {
center, center,
required, required: !!required,
clickable, clickable,
borderless: !border, borderless: !border,
}; };

View File

@ -8,7 +8,7 @@ exports[`should render demo and match snapshot 1`] = `
<div class="van-contact-edit__fields"> <div class="van-contact-edit__fields">
<div class="van-cell van-field"> <div class="van-cell van-field">
<div <div
class="van-cell__title van-field__label van-field__label--required" class="van-cell__title van-field__label"
style style
> >
<!--[--> <!--[-->

View File

@ -5,7 +5,7 @@ exports[`should render demo and match snapshot 1`] = `
<form class="van-form van-contact-edit"> <form class="van-form van-contact-edit">
<div class="van-contact-edit__fields"> <div class="van-contact-edit__fields">
<div class="van-cell van-field"> <div class="van-cell van-field">
<div class="van-cell__title van-field__label van-field__label--required"> <div class="van-cell__title van-field__label">
<label <label
id="van-field-label" id="van-field-label"
for="van-field-input" for="van-field-input"

View File

@ -91,12 +91,12 @@ export const fieldSharedProps = {
autocorrect: String, autocorrect: String,
errorMessage: String, errorMessage: String,
enterkeyhint: String, enterkeyhint: String,
clearTrigger: makeStringProp<FieldClearTrigger>('focus'),
formatTrigger: makeStringProp<FieldFormatTrigger>('onChange'),
spellcheck: { spellcheck: {
type: Boolean, type: Boolean,
default: null, default: null,
}, },
clearTrigger: makeStringProp<FieldClearTrigger>('focus'),
formatTrigger: makeStringProp<FieldFormatTrigger>('onChange'),
error: { error: {
type: Boolean, type: Boolean,
default: null, default: null,
@ -193,8 +193,12 @@ export default defineComponent({
return props.modelValue; return props.modelValue;
}); });
const isRequired = computed(() => { const showRequiredMark = computed(() => {
return props.rules?.some((rule: FieldRule) => rule.required); const required = getProp('required');
if (required === 'auto') {
return props.rules?.some((rule: FieldRule) => rule.required);
}
return required;
}); });
const runRules = (rules: FieldRule[]) => const runRules = (rules: FieldRule[]) =>
@ -700,10 +704,7 @@ export default defineComponent({
titleStyle={labelStyle.value} titleStyle={labelStyle.value}
valueClass={bem('value')} valueClass={bem('value')}
titleClass={[ titleClass={[
bem('label', [ bem('label', [labelAlign, { required: showRequiredMark.value }]),
labelAlign,
{ required: isRequired.value || props.required },
]),
props.labelClass, props.labelClass,
]} ]}
arrowDirection={props.arrowDirection} arrowDirection={props.arrowDirection}

View File

@ -115,22 +115,54 @@ export default {
}; };
``` ```
### Required
Use the `required` prop to display a required asterisk.
```html
<van-cell-group inset>
<van-field
v-model="username"
required
label="Username"
placeholder="Username"
/>
<van-field v-model="phone" required label="Phone" placeholder="Phone" />
</van-cell-group>
```
Please note that the `required` prop is only used for controlling the style. For form validation, you need to use the `rule.required` option to control the validation logic.
### Auto Required
You can set `required="auto"` on the Form component, and all the fields inside the Form will automatically display the asterisk based on the `rule.required` option.
```html
<van-form required="auto">
<van-field
v-model="username"
:rules="[{ required: true }]"
label="Username"
placeholder="Username"
/>
<van-field
v-model="phone"
:rules="[{ required: false }]"
label="Phone"
placeholder="Phone"
/>
</van-form>
```
### Error Info ### Error Info
Use `error` or `error-message` to show error info. Use `error` or `error-message` to show error info.
```html ```html
<van-cell-group inset> <van-cell-group inset>
<van-field <van-field v-model="username" error label="Username" placeholder="Username" />
v-model="username"
error
required
label="Username"
placeholder="Username"
/>
<van-field <van-field
v-model="phone" v-model="phone"
required
label="Phone" label="Phone"
placeholder="Phone" placeholder="Phone"
error-message="Invalid phone" error-message="Invalid phone"
@ -292,7 +324,7 @@ Use `label-align` prop to align the input value, can be set to `center`, `right`
| disabled | Whether to disable field | _boolean_ | `false` | | disabled | Whether to disable field | _boolean_ | `false` |
| readonly | Whether to be readonly | _boolean_ | `false` | | readonly | Whether to be readonly | _boolean_ | `false` |
| colon | Whether to display colon after label | _boolean_ | `false` | | colon | Whether to display colon after label | _boolean_ | `false` |
| required | Whether to show required mark | _boolean_ | `false` | | required | Whether to show required mark | _boolean \| 'auto'_ | `null` |
| center | Whether to center content vertically | _boolean_ | `true` | | center | Whether to center content vertically | _boolean_ | `true` |
| clearable | Whether to be clearable | _boolean_ | `false` | | clearable | Whether to be clearable | _boolean_ | `false` |
| clear-icon | Clear icon name | _string_ | `clear` | | clear-icon | Clear icon name | _string_ | `clear` |

View File

@ -125,6 +125,50 @@ export default {
}; };
``` ```
### 必填星号
设置 `required` 属性来展示必填星号。
```html
<van-cell-group inset>
<van-field
v-model="username"
required
label="用户名"
placeholder="请输入用户名"
/>
<van-field
v-model="phone"
required
label="手机号"
placeholder="请输入手机号"
/>
</van-cell-group>
```
请注意 `required` 属性只用于控制样式展示,在进行表单校验时,需要使用 `rule.required` 选项来控制校验逻辑。
### 自动展示星号
你可以在 Form 组件上设置 `required="auto"`,此时 Form 里的所有 Field 会自动根据 `rule.required` 来判断是否需要展示星号。
```html
<van-form required="auto">
<van-field
v-model="username"
:rules="[{ required: true }]"
label="用户名"
placeholder="请输入用户名"
/>
<van-field
v-model="phone"
:rules="[{ required: false }]"
label="手机号"
placeholder="请输入手机号"
/>
</van-form>
```
### 错误提示 ### 错误提示
设置 `required` 属性表示这是一个必填项,可以配合 `error``error-message` 属性显示对应的错误提示。 设置 `required` 属性表示这是一个必填项,可以配合 `error``error-message` 属性显示对应的错误提示。
@ -134,13 +178,11 @@ export default {
<van-field <van-field
v-model="username" v-model="username"
error error
required
label="用户名" label="用户名"
placeholder="请输入用户名" placeholder="请输入用户名"
/> />
<van-field <van-field
v-model="phone" v-model="phone"
required
label="手机号" label="手机号"
placeholder="请输入手机号" placeholder="请输入手机号"
error-message="手机号格式错误" error-message="手机号格式错误"
@ -311,7 +353,7 @@ export default {
| disabled | 是否禁用输入框 | _boolean_ | `false` | | disabled | 是否禁用输入框 | _boolean_ | `false` |
| readonly | 是否为只读状态,只读状态下无法输入内容 | _boolean_ | `false` | | readonly | 是否为只读状态,只读状态下无法输入内容 | _boolean_ | `false` |
| colon | 是否在 label 后面添加冒号 | _boolean_ | `false` | | colon | 是否在 label 后面添加冒号 | _boolean_ | `false` |
| required | 是否显示表单必填星号 | _boolean_ | `false` | | required | 是否显示表单必填星号 | _boolean \| 'auto'_ | `null` |
| center | 是否使内容垂直居中 | _boolean_ | `false` | | center | 是否使内容垂直居中 | _boolean_ | `false` |
| clearable | 是否启用清除图标,点击清除图标后会清空输入框 | _boolean_ | `false` | | clearable | 是否启用清除图标,点击清除图标后会清空输入框 | _boolean_ | `false` |
| clear-icon | 清除图标名称或图片链接,等同于 Icon 组件的 [name 属性](#/zh-CN/icon#props) | _string_ | `clear` | | clear-icon | 清除图标名称或图片链接,等同于 Icon 组件的 [name 属性](#/zh-CN/icon#props) | _string_ | `clear` |

View File

@ -31,13 +31,11 @@ const username = ref('');
<van-field <van-field
v-model="username" v-model="username"
error error
required
:label="t('username')" :label="t('username')"
:placeholder="t('usernamePlaceholder')" :placeholder="t('usernamePlaceholder')"
/> />
<van-field <van-field
v-model="phone" v-model="phone"
required
:label="t('phone')" :label="t('phone')"
:placeholder="t('phonePlaceholder')" :placeholder="t('phonePlaceholder')"
:error-message="t('phoneError')" :error-message="t('phoneError')"

View File

@ -0,0 +1,65 @@
<script setup lang="ts">
import VanField from '..';
import VanCellGroup from '../../cell-group';
import VanForm from '../../form';
import { ref } from 'vue';
import { useTranslate } from '../../../docs/site';
const t = useTranslate({
'zh-CN': {
phone: '手机号',
required: '必填星号',
autoRequired: '自动展示星号',
phonePlaceholder: '请输入手机号',
usernamePlaceholder: '请输入用户名',
},
'en-US': {
phone: 'Phone',
required: 'Required',
autoRequired: 'Auto Required',
phonePlaceholder: 'Phone',
usernamePlaceholder: 'Username',
},
});
const phone = ref('123');
const username = ref('');
</script>
<template>
<demo-block :title="t('required')">
<van-cell-group inset>
<van-field
v-model="username"
required
:label="t('username')"
:placeholder="t('usernamePlaceholder')"
/>
<van-field
v-model="phone"
required
:label="t('phone')"
:placeholder="t('phonePlaceholder')"
/>
</van-cell-group>
</demo-block>
<demo-block :title="t('autoRequired')">
<van-cell-group inset>
<van-form required="auto">
<van-field
v-model="username"
:rules="[{ required: true }]"
:label="t('username')"
:placeholder="t('usernamePlaceholder')"
/>
<van-field
v-model="phone"
:rules="[{ required: false }]"
:label="t('phone')"
:placeholder="t('phonePlaceholder')"
/>
</van-form>
</van-cell-group>
</demo-block>
</template>

View File

@ -3,6 +3,7 @@ import BasicUsage from './BasicUsage.vue';
import CustomType from './CustomType.vue'; import CustomType from './CustomType.vue';
import Disabled from './Disabled.vue'; import Disabled from './Disabled.vue';
import ShowIcon from './ShowIcon.vue'; import ShowIcon from './ShowIcon.vue';
import Required from './Required.vue';
import ErrorInfo from './ErrorInfo.vue'; import ErrorInfo from './ErrorInfo.vue';
import InsertButton from './InsertButton.vue'; import InsertButton from './InsertButton.vue';
import FormatValue from './FormatValue.vue'; import FormatValue from './FormatValue.vue';
@ -17,6 +18,7 @@ import LabelAlign from './LabelAlign.vue';
<custom-type /> <custom-type />
<disabled /> <disabled />
<show-icon /> <show-icon />
<required />
<error-info /> <error-info />
<insert-button /> <insert-button />
<format-value /> <format-value />

View File

@ -329,13 +329,137 @@ exports[`should render demo and match snapshot 1`] = `
</div> </div>
</div> </div>
</div> </div>
<!--[-->
<div>
<!--[-->
<div class="van-cell-group van-cell-group--inset">
<!--[-->
<div class="van-cell van-field">
<div
class="van-cell__title van-field__label van-field__label--required"
style
>
<!--[-->
<label
id="van-field-label"
for="van-field-input"
style
>
Username
</label>
</div>
<div class="van-cell__value van-field__value">
<!--[-->
<div class="van-field__body">
<input
type="text"
id="van-field-input"
class="van-field__control"
placeholder="Username"
aria-labelledby="van-field-label"
>
</div>
</div>
</div>
<div class="van-cell van-field">
<div
class="van-cell__title van-field__label van-field__label--required"
style
>
<!--[-->
<label
id="van-field-label"
for="van-field-input"
style
>
Phone
</label>
</div>
<div class="van-cell__value van-field__value">
<!--[-->
<div class="van-field__body">
<input
type="text"
id="van-field-input"
class="van-field__control"
placeholder="Phone"
aria-labelledby="van-field-label"
>
</div>
</div>
</div>
</div>
</div>
<div>
<!--[-->
<div class="van-cell-group van-cell-group--inset">
<!--[-->
<form class="van-form">
<!--[-->
<div class="van-cell van-field">
<div
class="van-cell__title van-field__label van-field__label--required"
style
>
<!--[-->
<label
id="van-field-label"
for="van-field-input"
style
>
Username
</label>
</div>
<div class="van-cell__value van-field__value">
<!--[-->
<div class="van-field__body">
<input
type="text"
id="van-field-input"
class="van-field__control"
placeholder="Username"
aria-labelledby="van-field-label"
>
</div>
</div>
</div>
<div class="van-cell van-field">
<div
class="van-cell__title van-field__label"
style
>
<!--[-->
<label
id="van-field-label"
for="van-field-input"
style
>
Phone
</label>
</div>
<div class="van-cell__value van-field__value">
<!--[-->
<div class="van-field__body">
<input
type="text"
id="van-field-input"
class="van-field__control"
placeholder="Phone"
aria-labelledby="van-field-label"
>
</div>
</div>
</div>
</form>
</div>
</div>
<div> <div>
<!--[--> <!--[-->
<div class="van-cell-group van-cell-group--inset"> <div class="van-cell-group van-cell-group--inset">
<!--[--> <!--[-->
<div class="van-cell van-field van-field--error"> <div class="van-cell van-field van-field--error">
<div <div
class="van-cell__title van-field__label van-field__label--required" class="van-cell__title van-field__label"
style style
> >
<!--[--> <!--[-->
@ -362,7 +486,7 @@ exports[`should render demo and match snapshot 1`] = `
</div> </div>
<div class="van-cell van-field"> <div class="van-cell van-field">
<div <div
class="van-cell__title van-field__label van-field__label--required" class="van-cell__title van-field__label"
style style
> >
<!--[--> <!--[-->

View File

@ -247,7 +247,7 @@ exports[`should render demo and match snapshot 1`] = `
</div> </div>
<div> <div>
<div class="van-cell-group van-cell-group--inset"> <div class="van-cell-group van-cell-group--inset">
<div class="van-cell van-field van-field--error"> <div class="van-cell van-field">
<div class="van-cell__title van-field__label van-field__label--required"> <div class="van-cell__title van-field__label van-field__label--required">
<label <label
id="van-field-label" id="van-field-label"
@ -256,6 +256,100 @@ exports[`should render demo and match snapshot 1`] = `
Username Username
</label> </label>
</div> </div>
<div class="van-cell__value van-field__value">
<div class="van-field__body">
<input
type="text"
id="van-field-input"
class="van-field__control"
placeholder="Username"
aria-labelledby="van-field-label"
>
</div>
</div>
</div>
<div class="van-cell van-field">
<div class="van-cell__title van-field__label van-field__label--required">
<label
id="van-field-label"
for="van-field-input"
>
Phone
</label>
</div>
<div class="van-cell__value van-field__value">
<div class="van-field__body">
<input
type="text"
id="van-field-input"
class="van-field__control"
placeholder="Phone"
aria-labelledby="van-field-label"
>
</div>
</div>
</div>
</div>
</div>
<div>
<div class="van-cell-group van-cell-group--inset">
<form class="van-form">
<div class="van-cell van-field">
<div class="van-cell__title van-field__label van-field__label--required">
<label
id="van-field-label"
for="van-field-input"
>
Username
</label>
</div>
<div class="van-cell__value van-field__value">
<div class="van-field__body">
<input
type="text"
id="van-field-input"
class="van-field__control"
placeholder="Username"
aria-labelledby="van-field-label"
>
</div>
</div>
</div>
<div class="van-cell van-field">
<div class="van-cell__title van-field__label">
<label
id="van-field-label"
for="van-field-input"
>
Phone
</label>
</div>
<div class="van-cell__value van-field__value">
<div class="van-field__body">
<input
type="text"
id="van-field-input"
class="van-field__control"
placeholder="Phone"
aria-labelledby="van-field-label"
>
</div>
</div>
</div>
</form>
</div>
</div>
<div>
<div class="van-cell-group van-cell-group--inset">
<div class="van-cell van-field van-field--error">
<div class="van-cell__title van-field__label">
<label
id="van-field-label"
for="van-field-input"
>
Username
</label>
</div>
<div class="van-cell__value van-field__value"> <div class="van-cell__value van-field__value">
<div class="van-field__body"> <div class="van-field__body">
<input <input
@ -269,7 +363,7 @@ exports[`should render demo and match snapshot 1`] = `
</div> </div>
</div> </div>
<div class="van-cell van-field"> <div class="van-cell van-field">
<div class="van-cell__title van-field__label van-field__label--required"> <div class="van-cell__title van-field__label">
<label <label
id="van-field-label" id="van-field-label"
for="van-field-input" for="van-field-input"

View File

@ -106,11 +106,13 @@ test('should render textarea when type is textarea', async () => {
await later(); await later();
expect(wrapper.html()).toMatchSnapshot(); expect(wrapper.html()).toMatchSnapshot();
}); });
test('should show required icon when using rules which contian required', async () => {
test('should show required icon when using rules which contain required', async () => {
const wrapper = mount(Field, { const wrapper = mount(Field, {
props: { props: {
modelValue: '123', modelValue: '123',
label: '123', label: '123',
required: 'auto',
rules: [{ required: false }], rules: [{ required: false }],
}, },
}); });
@ -119,6 +121,7 @@ test('should show required icon when using rules which contian required', async
await wrapper.setProps({ rules: [{ required: true }] }); await wrapper.setProps({ rules: [{ required: true }] });
expect(wrapper.find('.van-field__label--required').exists()).toBeTruthy(); expect(wrapper.find('.van-field__label--required').exists()).toBeTruthy();
}); });
test('should autosize textarea field', async () => { test('should autosize textarea field', async () => {
const wrapper = mount(Field, { const wrapper = mount(Field, {
props: { props: {

View File

@ -72,6 +72,7 @@ export type FieldValidationStatus = 'passed' | 'failed' | 'unvalidated';
// Shared props of Field and Form // Shared props of Field and Form
export type FieldFormSharedProps = export type FieldFormSharedProps =
| 'colon' | 'colon'
| 'required'
| 'disabled' | 'disabled'
| 'readonly' | 'readonly'
| 'labelWidth' | 'labelWidth'

View File

@ -28,6 +28,7 @@ export const formProps = {
colon: Boolean, colon: Boolean,
disabled: Boolean, disabled: Boolean,
readonly: Boolean, readonly: Boolean,
required: [Boolean, String] as PropType<boolean | 'auto'>,
showError: Boolean, showError: Boolean,
labelWidth: numericProp, labelWidth: numericProp,
labelAlign: String as PropType<FieldTextAlign>, labelAlign: String as PropType<FieldTextAlign>,

View File

@ -499,6 +499,7 @@ export default {
| colon | Whether to display colon after label | _boolean_ | `false` | | colon | Whether to display colon after label | _boolean_ | `false` |
| disabled | Whether to disable form | _boolean_ | `false` | | disabled | Whether to disable form | _boolean_ | `false` |
| readonly | Whether to be readonly | _boolean_ | `false` | | readonly | Whether to be readonly | _boolean_ | `false` |
| required `v4.7.3` | Whether to show required mark | _boolean \| 'auto'_ | `null` |
| validate-first | Whether to stop the validation when a rule fails | _boolean_ | `false` | | validate-first | Whether to stop the validation when a rule fails | _boolean_ | `false` |
| scroll-to-error | Whether to scroll to the error field when validation failed | _boolean_ | `false` | | scroll-to-error | Whether to scroll to the error field when validation failed | _boolean_ | `false` |
| show-error | Whether to highlight input when validation failed | _boolean_ | `false` | | show-error | Whether to highlight input when validation failed | _boolean_ | `false` |

View File

@ -531,6 +531,7 @@ export default {
| colon | 是否在 label 后面添加冒号 | _boolean_ | `false` | | colon | 是否在 label 后面添加冒号 | _boolean_ | `false` |
| disabled | 是否禁用表单中的所有输入框 | _boolean_ | `false` | | disabled | 是否禁用表单中的所有输入框 | _boolean_ | `false` |
| readonly | 是否将表单中的所有输入框设置为只读状态 | _boolean_ | `false` | | readonly | 是否将表单中的所有输入框设置为只读状态 | _boolean_ | `false` |
| required `v4.7.3` | 是否显示表单必填星号 | _boolean \| 'auto'_ | `null` |
| validate-first | 是否在某一项校验不通过时停止校验 | _boolean_ | `false` | | validate-first | 是否在某一项校验不通过时停止校验 | _boolean_ | `false` |
| scroll-to-error | 是否在提交表单且校验不通过时滚动至错误的表单项 | _boolean_ | `false` | | scroll-to-error | 是否在提交表单且校验不通过时滚动至错误的表单项 | _boolean_ | `false` |
| show-error | 是否在校验不通过时标红输入框 | _boolean_ | `false` | | show-error | 是否在校验不通过时标红输入框 | _boolean_ | `false` |

View File

@ -10,7 +10,7 @@ exports[`should render demo and match snapshot 1`] = `
<!--[--> <!--[-->
<div class="van-cell van-field"> <div class="van-cell van-field">
<div <div
class="van-cell__title van-field__label van-field__label--required" class="van-cell__title van-field__label"
style style
> >
<!--[--> <!--[-->
@ -38,7 +38,7 @@ exports[`should render demo and match snapshot 1`] = `
</div> </div>
<div class="van-cell van-field"> <div class="van-cell van-field">
<div <div
class="van-cell__title van-field__label van-field__label--required" class="van-cell__title van-field__label"
style style
> >
<!--[--> <!--[-->

View File

@ -5,7 +5,7 @@ exports[`should render demo and match snapshot 1`] = `
<form class="van-form"> <form class="van-form">
<div class="van-cell-group van-cell-group--inset"> <div class="van-cell-group van-cell-group--inset">
<div class="van-cell van-field"> <div class="van-cell van-field">
<div class="van-cell__title van-field__label van-field__label--required"> <div class="van-cell__title van-field__label">
<label <label
id="van-field-label" id="van-field-label"
for="van-field-input" for="van-field-input"
@ -27,7 +27,7 @@ exports[`should render demo and match snapshot 1`] = `
</div> </div>
</div> </div>
<div class="van-cell van-field"> <div class="van-cell van-field">
<div class="van-cell__title van-field__label van-field__label--required"> <div class="van-cell__title van-field__label">
<label <label
id="van-field-label" id="van-field-label"
for="van-field-input" for="van-field-input"