Search code examples
vue.jsdata-bindingvue-componentvue-composition-api

How to use v-model with custom components in Vue 2.7 Composition API?


I am building a custom input component, using Vue 2.7 and the Composition API. So far, my componente has come to this state below:

<template>
  <div class="ui-field">
    <label :for="id">{{ label }}{{ required ? '*' : '' }}</label>
    <input
      :aria-errormessage="errorDescriptionId()"
      :aria-invalid="!!error ? 'true' : 'false'"
      :autocomplete="autocomplete"
      class="ui-input"
      :data-acting="is_acting"
      :disabled="disabled"
      :id="id"
      :required="required"
      :type="type"
      :value="modelValue"
      @input="emitChange"
    />
    <p :id="errorDescriptionId()" v-if="error">{{ error }}</p>
  </div>
</template>

<script lang="ts" setup>
type UIInput = {
  autocomplete?: 'on' | 'off' | undefined
  disabled?: boolean | undefined
  error?: string | undefined
  id?: string | undefined
  is_acting?: boolean | undefined
  label?: string | undefined
  required?: boolean | undefined
  type?: 'text' | 'number' | undefined
  modelValue?: any
}

const props = defineProps<UIInput>()
const emit = defineEmits(['update:modelValue'])

function emitChange(event: Event): void {
  if (event?.target?.value) {
    emit('update:modelValue', event.target.value)
  }
}

function errorDescriptionId(): string | undefined {
  if (props.error === undefined) {
    return undefined
  }

  return `${!!props.id ? props.id : generateRandomId()}-error`
}

function generateRandomId(): string {
  const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
  let random_string = ''

  for (let i = 0; i < 4; i++) {
    const index = Math.floor(Math.random() * letters.length)
    random_string += letters.charAt(index)
  }

  return random_string
}
</script>

As you can see, the component should be able to receive a v-model on the parent, like this:

<UIInput
  autocomplete="off"
  id="complement"
  label="Complemento"
  type="text"
  v-model="form.address.complement"
/>

The form.address.complement is a reactive object, and its value should be passed to my component's input element and changed as I change the input the text on the input. All of that is how the component should behave, but in reality, when checking Vue DevTools, my UIInput has a modelValue: undefined, even though form.address.complement already starts with a string value, and also a $attrs: value: [value of form.address.complement]. It's like the two-way binding isn't connecting everything properly.

I don't know what I am doing wrong, so if anyone can help, I'm thankful.


Solution

  • I found out how to work this out. The only things to change are the emit, that needs to emit an event called "input"

    emit('input', 'event.target.value')
    

    and, in the type UIInput, I dont need a modelValue, as @Estus Flask commented on my question, but, actually, a value, which serves as value to :value in the input.