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

73 views Asked by At

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? ......................................................................................................

2

There are 2 answers

2
imhvost On BEST ANSWER

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>
1
Славик Гусев On

I implemented code that generally functions as I need. but it seems to me that there is a better option. Tell me how?

import type { Field } from '@/types/Field'
import { computed, ref } from 'vue'
import type { Ref, UnwrapRef } from 'vue'
import { validateField } from '@/helpers/validateField'
import CustomInput from '@/components/CustomInput.vue'


export const useForm = (formFields: Field[],
                        inputs: Ref<UnwrapRef<InstanceType<typeof CustomInput>>>) => {
  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 focus = () => {
    const arr = fieldsForm.value.findIndex((item) => {
      return item.errorMessages.length > 0
    })
    if (arr !== -1 && inputs.value[arr] && inputs.value[arr].input) {
      inputs.value[arr].input.focus()
    }
  }
  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 = () => {
    if (isFormValid.value) return
    checkFields()
    focus()
  }

  return {
    submit,
    isFormValid,
    fieldsForm
  }
}
<script setup lang="ts">
import CustomInput from '@/components/CustomInput.vue'
import { useForm } from '@/hooks/useForm'
import { ref, watchEffect } from 'vue'
import type { Field } from '@/types/Field'

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 inputs = ref<InstanceType<typeof CustomInput>>([])
const { isFormValid, fieldsForm, submit } = useForm(formData, inputs)


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

</script>

<template>
  <form @submit.prevent="submitForm">
    <CustomInput v-for="(data,key) in fieldsForm"
                 :key="data.name"
                 :ref="(el:HTMLInputElement|null) => inputs[key] = el"
                 :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, onUpdated, 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 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)
}
const input = ref<HTMLInputElement>()

defineExpose({
  input,
})

</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"
                 ref="input"
                 :name="field.name"
                 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>