Home » vue » Vue mutate prop binded by v-bind with sync modifier

Vue mutate prop binded by v-bind with sync modifier

Posted by: admin November 26, 2021 Leave a comment

Questions:

I have an object in my component data. Now, I’m just binding all the properties of the object as a prop to the child component using v-bind.sync directive. I’m updating these props from the child component using the built-in update event but still, I’m getting Avoid mutation props directly error in the console. Here is the minimal example attached.

Parent Component

<template>
  <div>
    <oslo v-bind.sync="data" />
  </div>
</template>

<script>

import Oslo from '@/components/Oslo.vue'

export default {
  components: {
    Oslo,
  },
  name: 'OsloParent',
  data() {
    return {
      data: {
        data: {
          name: 'Oslo name',
          access: 'admin'
        }
      },
    }
  },
}
</script>

Child component

<template>
  <div>
    <input type="text" v-model="name" @keyup="$emit('update:name', name)" />
    <input type="text" v-model="access" @keyup="$emit('update:access', access)" />
  </div>
</template>

<script>
export default {
  props: {
    name: String,
    access: String
  },
  name: 'Oslo',
}
</script>

This is just an example component I’ve created for the reproduction of the problem. The actual component is supposed to handle so many props with two-way binding and that’s the reason I’m binding the data with v-bind directive with sync modifier. Here is the Vue warning from the console (most common).

[Vue warn]: Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "name"

Any suggestions to improve this or silent the Vue warn for this specific case? The above-given components works as desired, Vue throws error though.

Answers:

I found two problems with your example that might throw this off.

  1. The use of v-model directly to the property. Use v-bind instead to have it only display. And use v-on:change handler to fire the $emit('update:propertyname', value) and send the new value to update on the object.

  2. The value sent along in the $emit seems empty and thus makes no change. Use $event.target.value instead.

Side note: v-on:keyup might not be the best event to listen to, since input can also be drag-and-dropped. Listening to v-on:change would be beter in that case.

Note on event listeners when using only v-bind.sync instead of v-bind:propertyName.sync:

If you want to listen to the update:propertyName event from the child component on the parent, you have to use the .capture modifier. Otherwise the update event is caught by the v-on:update:propertyName on the child component and this does not bubble up to the parent.

So you can use v-on:update:name.capture="someMethod" on the <oslo> tag for example. And have this someMethod in the parent’s methods. After this is called, the event will be triggered on the child component which will update the object and thereby the property.

All together:

let Oslo = {
  props: {
    name: String,
    access: String
  },
  name: 'Oslo',
  template: `<div>
    <input type="text" :value="name" @change="$emit('update:name', $event.target.value)" />
    <input type="text" :value="access" @change="$emit('update:access', $event.target.value)" />
  </div>`
}

new Vue({
  el: "#app",
  components: {
    Oslo,
  },
  data: {
    thedata: {
      name: 'Oslo name',
      access: 'admin'
    }
  },
  methods: {
    nameWillBeUpdated: function(v) {
      console.log('New value of name will be:', v);
      // After this, the `update:name` event handler of the
      // child component is triggered and the value will change.
    },
  },
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.min.js"></script>
<div id="app">
<span>{{this.thedata.name}} - {{this.thedata.access}}</span>
<oslo
    v-bind.sync="thedata"
    v-on:update:name.capture="nameWillBeUpdated"
/>
</div>

###

Try like following:

Vue.component('Oslo', {
  template: `
    <div>
    <input type="text" v-model="namec" @keyup="$emit('updated', {dat:namec, id: 'name'})" />
    <input type="text" v-model="accessc" @keyup="$emit('updated', {dat:accessc, id: ''})" />
  </div>
  `,
  props: {
    name: String,
    access: String
  },
  data() {
    return {
      namec: this.name,
      accessc: this.access
    }
  }
})

new Vue({
  el: '#demo',
      data() {
        return {
          name: 'Oslo name',
          access: 'admin'
        }
      },
  methods: {
    updated(data){
      data.id === 'name' ? this.name= data.dat : this.access= data.dat
      
    }
  }
})

Vue.config.productionTip = false
Vue.config.devtools = false
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="demo">
  <div>
  {{name}} {{access}}
    <oslo @updated="updated" />
    
  </div>
</div>

###

You can just pass an object and sync it instead of individual properties if you have many properties to listen to from child component. See the example below:

Vue.config.productionTip = false
Vue.config.devtools = false

Vue.component('Oslo', {
  template: `
    <div>
    <input type="text" v-model="comp_name" @keyup="$emit('update:name', comp_name)" />
    <input type="text" v-model="comp_access" @keyup="$emit('update:access', comp_access)" />
  </div>
  `,
  props: {
    data: {
      name: String,
      access: String,
    }
  },
  data() {
    return {
      comp_name: this.data.name,
      comp_access: this.data.access
    }
  }
})

new Vue({
  el: '#app',
  data() {
    return {
      doc: {
        name: 'Oslo name',
        access: 'admin'
      }
    }
  }
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
  <div>
    <span>---- {{ this.doc.name }}----</span>
    <span>---- {{ this.doc.access }}----</span>
    <oslo :data="this.doc" v-bind.sync="doc" />

  </div>
</div>