Search code examples
javascriptvue.jsvuejs3vuetify.js

Vue 3 form input flow breaks when emit is called on blur


In Vue 3, when emitting an event from child to parent to update refs and subsequently refresh the props of the child the form control flow breaks i.e. tabindex or [tab]ing between fields doesn't work. My code is roughly setup as follows:

Parent.vue

<script setup>
import { ref } from 'vue'
import Child from './Child.vue'
const connection = ref(null)

const change = (updated) => {
    connection.value = updated
}
</script>
<template>
    <Child :connection="connection" @update="change" />
</template>

Child.vue

<script setup>
import { ref } from 'vue'

const props = defineProps({
    connection: Object,
})

const emit = defineEmits(['update'])

const a = ref(props.connection?.a)
const b = ref(props.connection?.b)
const c = ref(props.connection?.c)

const update = () => {
    emit('update', {
        a: a.value,
        b: b.value,
        c: c.value,
    })
}
</script>
<template>
    <input v-model="a" @blur="update" />
    <input v-model="b" @blur="update" />
    <input v-model="c" @blur="update" />
</template>

I'm guessing due to it being an object that it breaks the the flow as it re-renders the whole child and loses focus. Question is how can I update the object and keep the focus the same?

Side note: I don't want to emit individual model value events like below because I don't want the parent to have to deal with every property since I'm creating several distinct Child vues for different forms.

<script setup>
defineProps({
  firstName: String,
  lastName: String
})

defineEmits(['update:firstName', 'update:lastName'])
</script>

<template>
  <input
    type="text"
    :value="firstName"
    @input="$emit('update:firstName', $event.target.value)"
  />
  <input
    type="text"
    :value="lastName"
    @input="$emit('update:lastName', $event.target.value)"
  />
</template>

This would lead to a parent like this and I'll end up having to handle a-z

...
<Child 
    v-model:a="connection.a"  
    v-model:b="connection.b"
    v-model:c="connection.c"
...

Solution

  • Try to simplify the Child component state with a writable computed that get the prop values and emit the new values when it's mutated :

    <script setup>
    import { computed } from 'vue'
    
    const props = defineProps({
      modelValue: Object,
    })
    
    const emit = defineEmits(['update:modelValue'])
    
    const model = computed({
      get: () => props.modelValue,
      set: (val) => {
        console.log(val)
        emit('update:modelValue', val)
      }
    })
    </script>
    <template>
      <input v-model="model.a" @blur="update" />
      <input v-model="model.b" @blur="update" />
      <input v-model="model.c" @blur="update" />
    </template>
    
    

    Parent component :

    <script setup>
    import { ref } from 'vue'
    import Child from './Child.vue'
    const connection = ref({})
    
    </script>
    <template>
        <Child v-model="connection"  />
    </template>
    

    LIVE DEMO