Jack Matthews
Jack Matthews

Reputation: 13

Vue3 Custom Component v-model doesn't work inside a div

I have written a custom Vue3 component for form input, and parent components should be able to use v-model to bind the value of the input field. I have done this successfully in basic form (which is shown in my first snippet below). However, it only seems to work when the input element is the parent element in the component template. If I surround the input element in a div (within the custom component), it stops working.

This is the working component in its basic form.

<template>
    <input v-model="value" class="form-control form-in" type="text" :name="name" :placeholder="placeholder" @input="updateValue()"/>
</template>

<script>
    export default {
        name: 'FormInput',
        props: {
            name: String,
            placeholder: String,
        },
        data () {
            return {
                value: ""
            }
        },
        methods: {
            updateValue() {
                this.$emit('update:modelValue', this.value)
            }
        }
    }
</script>

<style lang="scss" scoped>

</style>

This is utilised in a parent Vue file.

<div class="row mb-3">
    <div class="col-md-12">
        <label class="ml-2">Name</label>
            <FormInput
                name="name"
                v-model="form.name"
                ref="name"
            />
        </div>
    </div>
</div>

This is all working as expected. When I type in this input, the value of 'form.name' is being updated correctly. However, I wanted to add some more features to my input component, e.g. a label afterwards that could be used for a validation message. As a Vue component requires a single parent component, I surround both the input and label elements in a div.

<template>
    <div>
        <input v-model="value" :class="{ 'validation-failed' : validation_failed == true}" class="form-control form-in" type="text" :name="name" :placeholder="placeholder" @input="updateValue()"/>
        <label class="font-weight-bold ml-2 mt-1 text-danger" v-if="validation_failed">{{ validation_msg }}</label>
    </div>
</template>

I notice now that the v-model binding on the parent component is no longer working. To troubleshoot I rolled this all back, and started by just adding the div tag:

<template>
    <div>
        <input v-model="value" class="form-control form-in" type="text" :name="name" :placeholder="placeholder" @input="updateValue()"/>
    </div>
</template>

This also isn't working, so it's to do with the input being inside a div.

When I debug the 'FormInput' component, I can see its 'value' is being updated with the text input.

I'm assuming I'm missing some fundamental rule of Vue as to why this is not working, but I haven't been able to figure this out from Googling.

Upvotes: 1

Views: 2753

Answers (1)

Alexander Nenashev
Alexander Nenashev

Reputation: 22769

Vue3 SFC Playground

You could make some tweaks here:

  • Vue 3 supports multiple root elements in component templates
  • Since you use v-model on <input> there's no need to use its @input otherwise do just 1-way binding without v-model
  • But better use a standard v-model implementation with computed
  • You don't need an extra validation_failed since the presence of validation_msg is enough
  • You could consider Composition API with <script setup> as the latest recommend way to make components in Vue 3.
  • You could consider move <script> as the first section, again the latest recommended way
<template>
        <input v-model="value" :class="{ 'validation-failed' : validationMsg}" class="form-control form-in" type="text" :name="name" :placeholder="placeholder"/>
        <label class="font-weight-bold ml-2 mt-1 text-danger" v-if="validationMsg">{{ validationMsg }}</label>
</template>

<script>
    export default {
        name: 'FormInput',
        props: {
            name: String,
            placeholder: String,
            modelValue: String
        },
        computed: {
            validationMsg(){
                return this.value.match(/^\d*$/) ? '' : 'Not a number!!!'
            },
            value: {
                get(){
                    return this.modelValue;
                },
                set(value){
                    this.$emit('update:modelValue', value)
                }
            }
        }
    }
</script>

Upvotes: 0

Related Questions