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,
isLink: Boolean,
border: truthProp,
required: Boolean,
iconPrefix: String,
valueClass: unknownProp,
labelClass: unknownProp,
titleClass: unknownProp,
titleStyle: null as unknown as PropType<string | CSSProperties>,
arrowDirection: String as PropType<CellArrowDirection>,
required: {
type: [Boolean, String] as PropType<boolean | 'auto'>,
default: null,
},
clickable: {
type: Boolean as PropType<boolean | null>,
default: null,
@ -147,7 +150,7 @@ export default defineComponent({
const classes: Record<string, boolean | undefined> = {
center,
required,
required: !!required,
clickable,
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-cell van-field">
<div
class="van-cell__title van-field__label van-field__label--required"
class="van-cell__title van-field__label"
style
>
<!--[-->

View File

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

View File

@ -91,12 +91,12 @@ export const fieldSharedProps = {
autocorrect: String,
errorMessage: String,
enterkeyhint: String,
clearTrigger: makeStringProp<FieldClearTrigger>('focus'),
formatTrigger: makeStringProp<FieldFormatTrigger>('onChange'),
spellcheck: {
type: Boolean,
default: null,
},
clearTrigger: makeStringProp<FieldClearTrigger>('focus'),
formatTrigger: makeStringProp<FieldFormatTrigger>('onChange'),
error: {
type: Boolean,
default: null,
@ -193,8 +193,12 @@ export default defineComponent({
return props.modelValue;
});
const isRequired = computed(() => {
return props.rules?.some((rule: FieldRule) => rule.required);
const showRequiredMark = computed(() => {
const required = getProp('required');
if (required === 'auto') {
return props.rules?.some((rule: FieldRule) => rule.required);
}
return required;
});
const runRules = (rules: FieldRule[]) =>
@ -700,10 +704,7 @@ export default defineComponent({
titleStyle={labelStyle.value}
valueClass={bem('value')}
titleClass={[
bem('label', [
labelAlign,
{ required: isRequired.value || props.required },
]),
bem('label', [labelAlign, { required: showRequiredMark.value }]),
props.labelClass,
]}
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
Use `error` or `error-message` to show error info.
```html
<van-cell-group inset>
<van-field
v-model="username"
error
required
label="Username"
placeholder="Username"
/>
<van-field v-model="username" error label="Username" placeholder="Username" />
<van-field
v-model="phone"
required
label="Phone"
placeholder="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` |
| readonly | Whether to be readonly | _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` |
| clearable | Whether to be clearable | _boolean_ | `false` |
| 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` 属性显示对应的错误提示。
@ -134,13 +178,11 @@ export default {
<van-field
v-model="username"
error
required
label="用户名"
placeholder="请输入用户名"
/>
<van-field
v-model="phone"
required
label="手机号"
placeholder="请输入手机号"
error-message="手机号格式错误"
@ -311,7 +353,7 @@ export default {
| disabled | 是否禁用输入框 | _boolean_ | `false` |
| readonly | 是否为只读状态,只读状态下无法输入内容 | _boolean_ | `false` |
| colon | 是否在 label 后面添加冒号 | _boolean_ | `false` |
| required | 是否显示表单必填星号 | _boolean_ | `false` |
| required | 是否显示表单必填星号 | _boolean \| 'auto'_ | `null` |
| center | 是否使内容垂直居中 | _boolean_ | `false` |
| clearable | 是否启用清除图标,点击清除图标后会清空输入框 | _boolean_ | `false` |
| clear-icon | 清除图标名称或图片链接,等同于 Icon 组件的 [name 属性](#/zh-CN/icon#props) | _string_ | `clear` |

View File

@ -31,13 +31,11 @@ const username = ref('');
<van-field
v-model="username"
error
required
:label="t('username')"
:placeholder="t('usernamePlaceholder')"
/>
<van-field
v-model="phone"
required
:label="t('phone')"
:placeholder="t('phonePlaceholder')"
: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 Disabled from './Disabled.vue';
import ShowIcon from './ShowIcon.vue';
import Required from './Required.vue';
import ErrorInfo from './ErrorInfo.vue';
import InsertButton from './InsertButton.vue';
import FormatValue from './FormatValue.vue';
@ -17,6 +18,7 @@ import LabelAlign from './LabelAlign.vue';
<custom-type />
<disabled />
<show-icon />
<required />
<error-info />
<insert-button />
<format-value />

View File

@ -329,13 +329,137 @@ 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 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 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 van-field__label--required"
class="van-cell__title van-field__label"
style
>
<!--[-->
@ -362,7 +486,7 @@ exports[`should render demo and match snapshot 1`] = `
</div>
<div class="van-cell van-field">
<div
class="van-cell__title van-field__label van-field__label--required"
class="van-cell__title van-field__label"
style
>
<!--[-->

View File

@ -247,7 +247,7 @@ exports[`should render demo and match snapshot 1`] = `
</div>
<div>
<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">
<label
id="van-field-label"
@ -256,6 +256,100 @@ exports[`should render demo and match snapshot 1`] = `
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">
<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-field__body">
<input
@ -269,7 +363,7 @@ exports[`should render demo and match snapshot 1`] = `
</div>
</div>
<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
id="van-field-label"
for="van-field-input"

View File

@ -106,11 +106,13 @@ test('should render textarea when type is textarea', async () => {
await later();
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, {
props: {
modelValue: '123',
label: '123',
required: 'auto',
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 }] });
expect(wrapper.find('.van-field__label--required').exists()).toBeTruthy();
});
test('should autosize textarea field', async () => {
const wrapper = mount(Field, {
props: {

View File

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

View File

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

View File

@ -499,6 +499,7 @@ export default {
| colon | Whether to display colon after label | _boolean_ | `false` |
| disabled | Whether to disable form | _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` |
| 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` |

View File

@ -531,6 +531,7 @@ export default {
| colon | 是否在 label 后面添加冒号 | _boolean_ | `false` |
| disabled | 是否禁用表单中的所有输入框 | _boolean_ | `false` |
| readonly | 是否将表单中的所有输入框设置为只读状态 | _boolean_ | `false` |
| required `v4.7.3` | 是否显示表单必填星号 | _boolean \| 'auto'_ | `null` |
| validate-first | 是否在某一项校验不通过时停止校验 | _boolean_ | `false` |
| scroll-to-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__title van-field__label van-field__label--required"
class="van-cell__title van-field__label"
style
>
<!--[-->
@ -38,7 +38,7 @@ exports[`should render demo and match snapshot 1`] = `
</div>
<div class="van-cell van-field">
<div
class="van-cell__title van-field__label van-field__label--required"
class="van-cell__title van-field__label"
style
>
<!--[-->

View File

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