I'm trying to build a custom select input, the functionality works as expected but not with the validation. I can't get it work by following the example since a <button>
can't have a v-model
. I don't think I can use a computed get-set
approach like shown in the docs because I only use computed prop to get the text of the selected value to be shown in the UI, not to be sent to parent, nor to set a value.
Setting the value are done using @click="$emit(...)"
on <li>
element.
To be clear, here I provide both the script and the implementation, please have a look
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>
So, is this possible to validate with vee?