如何使用 VeeValidate 验证由按钮组成的自定义 select 组件?
How can I validate a custom select component made of a button using VeeValidate?
我正在尝试构建自定义 select 输入,该功能按预期工作但未通过验证。我无法按照示例进行操作,因为 <button>
不能有 v-model
。我不认为我可以使用计算 get-set
方法 like shown in the docs 因为我只使用计算 prop 来获取要在 UI 中显示的 selected 值的文本, 不会发送给父级,也不会设置值。
使用 <li>
元素上的 @click="$emit(...)"
设置值。
为了清楚起见,我在这里提供了脚本和实现,请看一下
BaseSelect.vue
<template>
<ValidationProvider
tag="div"
:vid="name"
:mode="validationMode"
:rules="validationRules"
v-slot="{ errors }"
>
<label :id="name" class="block font-medium mx-2 mb-3">{{ label }}</label>
<div class="relative">
<button
type="button"
aria-haspopup="listbox"
:aria-expanded="open"
:aria-labelledby="name"
class="relative w-full bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700 rounded-md shadow-sm pl-3 pr-10 py-2 text-left cursor-default focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
@click.prevent="open = !open"
>
<span v-if="value">{{ selected }}</span>
<span v-else>{{ placeholder }}</span>
<!-- selectable indicator icon (svg) -->
...
</button>
<!-- Error message -->
<small v-if="errors.length > 0" class="text-red-500 mt-3 mx-2">{{
errors[0]
}}</small>
<transition
leave-active-class="transition ease-in duration-100"
leave-class="opacity-100"
leave-to-class="opacity-0"
>
<div
class="absolute mt-1 w-full rounded-md bg-white dark:bg-gray-800 shadow-lg"
v-show="open"
>
<ul
tabindex="-1"
role="listbox"
:aria-labelledby="name"
aria-activedescendant="listbox-item-3"
class="max-h-56 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm"
>
<li
v-for="item in items"
:key="item.id"
:id="`listbox-item-${item.id}`"
role="option"
class="text-gray-900 dark:text-white hover:bg-gray-200 dark:hover:bg-gray-700 cursor-default select-none relative py-2 pl-3 pr-9"
@click="$emit('input', item[preferedValue])"
>
<span
class="ml-3 block font-normal truncate"
v-text="item.name"
/>
<!-- Checkmark, only display for selected option. (svg) -->
...
</li>
</ul>
</div>
</transition>
</div>
</ValidationProvider>
</template>
<script>
import { ValidationProvider } from 'vee-validate'
export default {
inheritAttrs: false,
components: { ValidationProvider },
props: {
name: {
type: String,
required: true,
},
label: {
type: String,
required: true,
},
hideLabel: {
type: Boolean,
default: false,
},
placeholder: {
type: String,
default: 'Choose one',
},
value: {
type: [String, Number, Object],
default: null,
},
required: {
type: Boolean,
default: true,
},
rules: {
type: String,
required: false,
},
validationMode: {
type: String,
default: 'eager',
},
items: {
type: Array,
required: true,
default: () => [],
},
preferedValue: {
type: String,
default: 'id',
},
},
data: () => ({
open: false,
}),
computed: {
/**
* The text to be shown based on the selected item and
* the prefered value as it's key.
*
* @returns string
*/
selected() {
if (this.value === null || !this.items.length) return ''
let index = _.findIndex(
this.items,
(item) => item[this.preferedValue] === this.value
)
return this.items[index].name
},
/**
* The validation rules to be applied in this input field.
*
* @returns string
*/
validationRules() {
if (!this.required) return this.rules
return this.rules ? `required|${this.rules}` : 'required'
},
},
mounted() {
document.addEventListener('click', this.close)
},
destroyed() {
document.removeEventListener('click', this.close)
},
methods: {
toggle() {
this.open = !this.open
},
close(e) {
if (!this.$el.contains(e.target)) {
this.open = false
}
},
},
}
</script>
implementation.vue
<template>
<BaseForm
ref="form"
reference="subcategory_form"
:errors="errors"
action="/subcategories"
method="POST"
@submit="store"
>
...
<BaseSelect
class="mb-4"
name="category_id"
label="Category"
placeholder="Choose one"
:items="categories.data"
v-model="subcategory.category_id"
/>
</BaseForm>
</template>
<script>
layout: 'dashboard',
async fetch() {
await this.$store.dispatch('categories/load')
},
data: () => ({
subcategory: {
category_id: null,
name: '',
},
errors: {},
}),
computed: {
categories() {
return this.$store.state.categories.pagination
},
},
methods: {
async store() {
...
},
},
}
</script>
BaseForm.vue
<template>
<ValidationObserver :ref="reference" tag="div" v-slot="{ handleSubmit }">
<form
:action="action"
:method="method"
@submit.prevent="handleSubmit(onSubmit)"
>
<slot></slot>
</form>
</ValidationObserver>
</template>
<script>
import { ValidationObserver } from 'vee-validate'
export default {
components: { ValidationObserver },
props: {
action: {
type: String,
required: true,
},
method: {
type: String,
required: true,
},
reference: {
type: String,
required: true,
},
errors: {
type: Object,
default: () => {},
},
},
watch: {
/**
* Watch for `errors`.
*
* Everytime it changes, assuming it comes from the backend,
* assign the errors to the `ValidationObserver`.
*/
errors(val) {
this.$refs[this.reference].setErrors(val)
},
},
methods: {
/**
* Emit `submit` event to the parent component.
*
* @returns void
*/
onSubmit() {
this.$emit('submit')
},
},
}
</script>
那么,这可以用 vee 验证吗?
您可以在 ValidationProvider
的插槽道具上使用 validate
功能,这里有一个指南。
https://vee-validate.logaretm.com/v3/advanced/model-less-validation.html#html-file-validation
验证函数接受要在内部设置为当前值的输入值,该值将被验证。
<ValidationProvider v-slot="{ validate }">
<button @click="validate(someValue)"></button>
</ValidationProvider>
我正在尝试构建自定义 select 输入,该功能按预期工作但未通过验证。我无法按照示例进行操作,因为 <button>
不能有 v-model
。我不认为我可以使用计算 get-set
方法 like shown in the docs 因为我只使用计算 prop 来获取要在 UI 中显示的 selected 值的文本, 不会发送给父级,也不会设置值。
使用 <li>
元素上的 @click="$emit(...)"
设置值。
为了清楚起见,我在这里提供了脚本和实现,请看一下
BaseSelect.vue
<template>
<ValidationProvider
tag="div"
:vid="name"
:mode="validationMode"
:rules="validationRules"
v-slot="{ errors }"
>
<label :id="name" class="block font-medium mx-2 mb-3">{{ label }}</label>
<div class="relative">
<button
type="button"
aria-haspopup="listbox"
:aria-expanded="open"
:aria-labelledby="name"
class="relative w-full bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700 rounded-md shadow-sm pl-3 pr-10 py-2 text-left cursor-default focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
@click.prevent="open = !open"
>
<span v-if="value">{{ selected }}</span>
<span v-else>{{ placeholder }}</span>
<!-- selectable indicator icon (svg) -->
...
</button>
<!-- Error message -->
<small v-if="errors.length > 0" class="text-red-500 mt-3 mx-2">{{
errors[0]
}}</small>
<transition
leave-active-class="transition ease-in duration-100"
leave-class="opacity-100"
leave-to-class="opacity-0"
>
<div
class="absolute mt-1 w-full rounded-md bg-white dark:bg-gray-800 shadow-lg"
v-show="open"
>
<ul
tabindex="-1"
role="listbox"
:aria-labelledby="name"
aria-activedescendant="listbox-item-3"
class="max-h-56 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm"
>
<li
v-for="item in items"
:key="item.id"
:id="`listbox-item-${item.id}`"
role="option"
class="text-gray-900 dark:text-white hover:bg-gray-200 dark:hover:bg-gray-700 cursor-default select-none relative py-2 pl-3 pr-9"
@click="$emit('input', item[preferedValue])"
>
<span
class="ml-3 block font-normal truncate"
v-text="item.name"
/>
<!-- Checkmark, only display for selected option. (svg) -->
...
</li>
</ul>
</div>
</transition>
</div>
</ValidationProvider>
</template>
<script>
import { ValidationProvider } from 'vee-validate'
export default {
inheritAttrs: false,
components: { ValidationProvider },
props: {
name: {
type: String,
required: true,
},
label: {
type: String,
required: true,
},
hideLabel: {
type: Boolean,
default: false,
},
placeholder: {
type: String,
default: 'Choose one',
},
value: {
type: [String, Number, Object],
default: null,
},
required: {
type: Boolean,
default: true,
},
rules: {
type: String,
required: false,
},
validationMode: {
type: String,
default: 'eager',
},
items: {
type: Array,
required: true,
default: () => [],
},
preferedValue: {
type: String,
default: 'id',
},
},
data: () => ({
open: false,
}),
computed: {
/**
* The text to be shown based on the selected item and
* the prefered value as it's key.
*
* @returns string
*/
selected() {
if (this.value === null || !this.items.length) return ''
let index = _.findIndex(
this.items,
(item) => item[this.preferedValue] === this.value
)
return this.items[index].name
},
/**
* The validation rules to be applied in this input field.
*
* @returns string
*/
validationRules() {
if (!this.required) return this.rules
return this.rules ? `required|${this.rules}` : 'required'
},
},
mounted() {
document.addEventListener('click', this.close)
},
destroyed() {
document.removeEventListener('click', this.close)
},
methods: {
toggle() {
this.open = !this.open
},
close(e) {
if (!this.$el.contains(e.target)) {
this.open = false
}
},
},
}
</script>
implementation.vue
<template>
<BaseForm
ref="form"
reference="subcategory_form"
:errors="errors"
action="/subcategories"
method="POST"
@submit="store"
>
...
<BaseSelect
class="mb-4"
name="category_id"
label="Category"
placeholder="Choose one"
:items="categories.data"
v-model="subcategory.category_id"
/>
</BaseForm>
</template>
<script>
layout: 'dashboard',
async fetch() {
await this.$store.dispatch('categories/load')
},
data: () => ({
subcategory: {
category_id: null,
name: '',
},
errors: {},
}),
computed: {
categories() {
return this.$store.state.categories.pagination
},
},
methods: {
async store() {
...
},
},
}
</script>
BaseForm.vue
<template>
<ValidationObserver :ref="reference" tag="div" v-slot="{ handleSubmit }">
<form
:action="action"
:method="method"
@submit.prevent="handleSubmit(onSubmit)"
>
<slot></slot>
</form>
</ValidationObserver>
</template>
<script>
import { ValidationObserver } from 'vee-validate'
export default {
components: { ValidationObserver },
props: {
action: {
type: String,
required: true,
},
method: {
type: String,
required: true,
},
reference: {
type: String,
required: true,
},
errors: {
type: Object,
default: () => {},
},
},
watch: {
/**
* Watch for `errors`.
*
* Everytime it changes, assuming it comes from the backend,
* assign the errors to the `ValidationObserver`.
*/
errors(val) {
this.$refs[this.reference].setErrors(val)
},
},
methods: {
/**
* Emit `submit` event to the parent component.
*
* @returns void
*/
onSubmit() {
this.$emit('submit')
},
},
}
</script>
那么,这可以用 vee 验证吗?
您可以在 ValidationProvider
的插槽道具上使用 validate
功能,这里有一个指南。
https://vee-validate.logaretm.com/v3/advanced/model-less-validation.html#html-file-validation
验证函数接受要在内部设置为当前值的输入值,该值将被验证。
<ValidationProvider v-slot="{ validate }">
<button @click="validate(someValue)"></button>
</ValidationProvider>