0
votes

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?

1

1 Answers

1
votes

You can use the validate function on the slot props of the ValidationProvider, there is a guide for this here.

https://vee-validate.logaretm.com/v3/advanced/model-less-validation.html#html-file-validation

The validate function accepts the input value to be set as the current value internally which will be validated.

<ValidationProvider v-slot="{ validate }">
  <button @click="validate(someValue)"></button>
</ValidationProvider>