vue.jsvuejs3refrefs

how to use both ref and v -for in my custom component in Vue 3?


I have field validation and during submit I need to focus on the First input field which has errors

from my parent component CustomForm I can't access my child component input in CustomInput

<script setup lang="ts">
import CustomForm from '@/components/CustomForm.vue'

</script>

<template>
  <CustomForm />
</template>

<style scoped lang="scss">
</style>

<script setup lang="ts">
import CustomInput from '@/components/CustomInput.vue'
import { useForm } from '@/hooks/useForm'

const formData = [
  {
    name: 'title',
    label: 'title',
    required: true,
    value: '',
    isValid: false,
    errorMessages: []
  },
  {
    name: 'name',
    label: 'name',
    required: true,
    type: 'textarea',
    value: '',
    isValid: false,
    errorMessages: []
  }
]
const { isFormValid, fieldsForm, submit } = useForm(formData)

const submitForm = () => {
  submit()
  if (isFormValid.value) {
    console.log('submit')
    console.log(fieldsForm)
  }
}


</script>

<template>
  <form @submit.prevent="submitForm">
    <CustomInput v-for="data in fieldsForm"
                 :key="data.name"
                 ref="customInputRef"
                 :field-data="data"
                 v-model="data.value"
                 v-model:error="data.errorMessages"
                 v-model:isValid="data.isValid"
    />
    <button type="submit">Отправить</button>
  </form>
</template>

<style scoped lang="scss">

</style>

<script setup lang="ts">
import { computed, ref, watchEffect } from 'vue'
import { v4 as uuidv4 } from 'uuid'
import type { IFieldProps } from '@/types/Field'
import { useInputValidator } from '@/hooks/useInputValidator'

const props = withDefaults(defineProps<IFieldProps>(), {
  placeholder: (props: IFieldProps) => `Input  ${props.fieldData.name.toUpperCase()} please`
})

const emit = defineEmits([
  'update:modelValue',
  'update:error',
  'update:isValid',
])
const r= ref(null)
const inputComponent = ref(props.fieldData.type !== 'textarea' ? 'input' : 'textarea')
const inputId = computed(() => `input-${uuidv4()}`)

const { isBlurred,field, blurAction,inputAction, errors, isValid } = useInputValidator(props.fieldData)

const inputHandler = (e: Event) => {
  const v = (e.target as HTMLInputElement).value
  emit('update:modelValue', v)
  inputAction()
  emit('update:error', errors)
}

const blurHandler = (e: Event) => {
  blurAction()
  emit('update:error', errors)
  emit('update:isValid', isValid)
}

</script>

<template>
  <div class="field">
    <label class="field__label"
           :for="inputId">
      {{ field.label }}
    </label>
    <div class="field__inner">
      <div class="field-icon" v-if="$slots.icon">
        <slot name="icon"></slot>
      </div>
      <component :is="inputComponent"
                 :name="field.name"
                 ref="r"
                 class="field__input"
                 :class="{
                   valid:field.isValid,
                   error:field.errorMessages.length,
                 }"
                 :id="inputId"
                 :value="field.value"
                 @input="inputHandler"
                 @blur="blurHandler"
                 :placeholder="props.placeholder" />
      <template v-if="field.errorMessages">
        <p class="field__error" v-for="(error,index) in field.errorMessages" :key="index">
          {{ error }}
        </p>
      </template>
    </div>
  </div>
</template>
import type { Field } from '@/types/Field'
import { computed, ref } from 'vue'
import { validateField } from '@/helpers/validateField'


export const useForm = (formFields: Field[]) => {
  const fieldsForm = ref(formFields)
  const isFormValid = computed(() =>
    fieldsForm.value.every(field => field.isValid)
  )


  const updateValidity = (fieldName: string, errors: string[]) => {
    const field = fieldsForm.value.find(data => data.name === fieldName)
    if (field) {
      field.errorMessages = errors
    }
  }

  const checkFields = () => {
    fieldsForm.value.forEach(field => {
      let err: string[] = []
      if (field.required) {
        if (!isFormValid.value && !field.errorMessages.length) {
          err = validateField(field.name, field.value)
          updateValidity(field.name, err)
        }
      }
    })
  }

  const submit = () => {
    checkFields()
  }

  return {
    submit,
    isFormValid,
    fieldsForm

  }
}

import { computed, ref, watchEffect } from 'vue'
import type { Field } from '@/types/Field'
import { validateField } from '@/helpers/validateField'

export const useInputValidator = (fieldForm: Field) => {
  const field = ref<Field>(fieldForm)
  const errors = ref(field.value.errorMessages)
  const isBlurred = ref(false)
  const isValid = computed(() => {
   return !errors.value.length
  })

  watchEffect(()=>{
    if(field.value.errorMessages.length){
      isBlurred.value= true
      errors.value= field.value.errorMessages
    }
  })

  const inputAction = () => {
    if (isBlurred.value) {
      errors.value = validateField(field.value.name, field.value.value)
    }
  }

  const blurAction = () => {
    if (isBlurred.value) return
    errors.value = validateField(field.value.name, field.value.value)
    isBlurred.value =true
  }

  return {
    field,
    isValid,
    errors,
    blurAction,
    inputAction,
isBlurred
  }
}

I verified the fields. But the functionality with focus remained. I would like to have access to form fields from hooks how to change the data of the main array for example isValid? ......................................................................................................


Solution

  • First, to apply Refs inside v-for use an array or function according to the documentation.

    Second, in the child component, use defineExpose() to bind to the required element Component Reference.

    I created simple example:

    /* App.vue */
    <script setup>
    import { ref } from 'vue';
    import CustomInput from './CustomInput.vue';
    
    const formData = ref({
      firstName: '',
      lastName: '', 
    })
    
    const inputs = ref({});
    
    const focus = (key) => {
      inputs.value[key].input.focus();
    }
    
    </script>
    
    <template>
      <div class="inputs">
        <CustomInput
          v-for="(value, key) in formData"
          v-model="formData[key]"
          :ref="el => inputs[key] = el"
          :label="key"
        ></CustomInput>
      </div>
      <br>
      <div class="btns">
        <button
          v-for="(value, key) in inputs"
          @click="focus(key)"
        >
          Focus to {{ key }}
        </button>
      </div>
      <br>
      <pre>{{ formData }}</pre>
    </template>
    
    <style>
    .inputs {
      display: grid;
      gap: 4px;
    }
    
    .btns {
      display: flex;
      gap: 8px;
    }
    </style>
    
    /* CustomInput.vue */
    
    <script setup>
    import { ref } from 'vue';
    
    defineProps({
      label: String,
    })
    
    const model = defineModel();
    const input = ref();
    
    defineExpose({ input });
    
    </script>
    
    <template>
      <label>
        {{ label }}:
        <br>
        <input
          ref="input"
          v-model="model"
        >
      </label>
    </template>