skip to Main Content

In my parent component I have something similar to this,

<template>
    <ProductCounter v-model="formData.productCount" label="product count" />
</template>

<script setup>

const initialFormData = {
    productCount: null,
    firstname: '',
    surname: '',
    phone: '',
    email: '',
    postcode: '',
    submittedOnce: false,
    errors: []
}

let formData = reactive({ ...initialFormData });

const clearUI = () => {
    formData = reactive({ ...initialFormData });
    triggerInlineForm.value = false;
}

</script>

My child component looks like this,

<template>
    <div class="form__row" @reset-counts="resetCount">
        <div class="counter__row">
            <label>{{ label }}</label>
            <div class="form__counter">
                <button class="form__button--decrease form__button--circle form__button--animate-scale" :disabled="value == 0 || props.disabled" @click.prevent="decreaseCount()">
                    <i>
                        <FontAwesomeIcon :icon="['fal', 'minus']" />
                    </i>
                </button>
                <input type="text" v-model="value" :disabled="props.disabled" @input="updateQty" placeholder="0"/>
                <button class="form__button--increase form__button--circle form__button--animate-scale" :disabled="props.disabled" @click.prevent="increaseCount()">
                    <i>
                        <FontAwesomeIcon :icon="['fal', 'plus']" />
                    </i>
                </button>
            </div>
        </div>
    </div>
</template>

<script setup>
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';

const emits = defineEmits(['update:modelValue', 'resetCounts']);

const props = defineProps({
    label: {
        type: String,
        required: true
    },
    modelValue: {
        type: String,
        required: true,
        default: 0
    },
    disabled: {
        type: Boolean,
        required: false
    }
});

const value = ref(props.modelValue);

const updateQty = () => {
    emits('update:modelValue', value.value)
}

const increaseCount = () => {
    value.value++
    emits('update:modelValue', value.value)
}

const decreaseCount = () => {
    value.value--;
    emits('update:modelValue', value.value)
}

</script>

I would expect that when clearUI is fired from the parent, and formData gets reset the v-model of ProductCounter should reflect that go back to 0 but it does not, where am I going wrong?

2

Answers


  1. The link to live solution

    Please prepare minimum reproducible example the next time on https://play.vuejs.org/. And to your question:

    You SHOULD NOT overwrite reactive variables in Vue please…

    Just mutate them Object.assign(formData, initialFormData):

    Also don’t dereference component properties: const value = ref(props.modelValue). The properties lose their reactivity because you just a copy a primitive value.

    The best way to create a v-model pattern is to use computed which you can manipulate directly in the template.

    const value = computed({
      get(){
        return props.modelValue;
      },
      set(val){
        emits('update:modelValue', val);
      }
    });
    

    Also your count property should be a number, not a string (you get Vue warnings):

        modelValue: {
            type: Number,
            required: true,
            default: 0
        },
    

    Also there’s no need to update the prop on the input event, since you’re already using v-model on the <input>. Also you should convert your input’s model to a number:

     <input type="text" v-model.number="value" :disabled="props.disabled" placeholder="0"/>
    

    So you have:
    App.vue

    <template>
        <p>
          <ProductCounter v-model="formData.productCount" label="product count" />
        </p>
        <button @click="clearUI">
          Clear
        </button>
        <div>
          {{ JSON.stringify(formData) }}
      </div>
    </template>
    
    <script setup>
    import ProductCounter from './ProductCounter.vue'
    import {reactive} from 'vue'
      
    const initialFormData = {
        productCount: 0,
        firstname: '',
        surname: '',
        phone: '',
        email: '',
        postcode: '',
        submittedOnce: false,
        errors: []
    }
    
    let formData = reactive({ ...initialFormData });
    
    const clearUI = () => {
        Object.assign(formData, initialFormData);
    }
    
    </script>
    

    ProductCounter.vue:

    <template>
        <div class="form__row">
            <div class="counter__row">
                <label>{{ label }}</label>
                <div class="form__counter">
                    <button :disabled="value == 0 || props.disabled" @click.prevent="value--">
                    -
                    </button>
                    <input type="text" v-model.number="value" :disabled="props.disabled" placeholder="0"/>
                    <button :disabled="props.disabled" @click.prevent="value++">
                     +
                    </button>
                </div>
            </div>
        </div>
    </template>
    
    <script setup>
    import {computed} from 'vue';
    const emits = defineEmits(['update:modelValue']);
    
    const props = defineProps({
        label: {
            type: String,
            required: true
        },
        modelValue: {
            type: Number,
            required: true,
            default: 0
        },
        disabled: {
            type: Boolean,
            required: false
        }
    });
    
    const value = computed({
      get(){
        return props.modelValue;
      },
      set(val){
        emits('update:modelValue', val);
      }
    });
    
    
    </script>
    
    Login or Signup to reply.
  2. When you override formData in clearUI(), you change the content of the variable:

    let formData = reactive({ ...initialFormData });
    
    const clearUI = () => {
        formData = reactive({ ...initialFormData });
    }
    

    However, this does not change the object that was bound to the template during component setup. You can fix this using ref and assigning to its value:

    const formData = ref({ ...initialFormData });
    
    const clearUI = () => {
        formData.value = { ...initialFormData };
    }
    

    or you can override the properties individually:

    const formData = reactive({ ...initialFormData });
    
    const clearUI = () => {
        Object.assign(formData, initialFormData);
    }
    

    A second problem is that you are setting the value in ProductCounter to the initial value of props.modelValue, but that is the literal value, not a reactive property. So when props.modelValue changes, value does not. To fix it, you can add a watcher:

    const value = ref(props.modelValue);
    watch(
      () => props.modelValue,
      () => value.value = props.modelValue
    )
    

    Now value will be adjusted when props.modelValue changes.

    Here it is in a playground

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search