11

I am trying to create a component that accepts an object as prop and can modify different properties of that object and return the value to the parent, using either sync or emit events. The example won't work, but it's simply to demonstrate what I'm trying to achieve.

Here's a snippet of what I'm trying to achieve :

Vue.component('child', {
  template: '#child',
  
  //The child has a prop named 'value'. v-model will automatically bind to this prop
  props: ['value'],
  methods: {
    updateValue: function (value) {
      //Not sure if both fields should call the same updateValue method that returns the complete object, or if they should be separate
      this.$emit('input', value);
    }
  }
});

new Vue({
  el: '#app',
  data: {
    parentObject: {value1: "1st Value", value2: "2nd value"}
  }
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.13/vue.js"></script>

<div id="app">
  <p>Parent value: {{parentObject}}</p>
  <child v-model="parentObject"></child>
</div>

<template id="child">
   <input type="text" v-bind:value.value1="value" v-on:input="updateValue($event.target.value)">
   <input type="text" v-bind:value.value2="value" v-on:input="updateValue($event.target.value)">
</template>
Storm
  • 387
  • 1
  • 5
  • 18

5 Answers5

18

You shouldn't modify the object being passed in as a prop. Instead, you should create a new data property in the child component and initialize it with a copy of the prop object.

Then, I would just use a v-model for each of the inputs in the child component and add a deep watcher to the internal value which would emit an update whenever the internal value changes.

Vue.component('child', {
  template: '#child',
  props: ['value'],
  data() {
    return { val: {...this.value} };
  },
  watch: {
    val: {
      deep: true,
      handler(value) {
        this.$emit('input', value);
      }
    }
  }
});

new Vue({
  el: '#app',
  data: {
    parentObject: {value1: "1st Value", value2: "2nd value"}
  }
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.13/vue.min.js"></script>

<div id="app">
  <p>Parent value: {{parentObject}}</p>
  <child v-model="parentObject"></child>
</div>

<template id="child">
  <div>
    <input type="text" v-model="val.value1">
    <input type="text" v-model="val.value2">
  </div>
</template>

I made a shallow copy of the prop in my example ({...this.value}), because the object doesn't have any nested properties. If that wasn't the case, you might need to do a deep copy (JSON.parse(JSON.stringify(this.value))).

thanksd
  • 54,176
  • 22
  • 157
  • 150
  • Hi, the code looks great, I tried implementing it, but as soon as I add the watch part to the component, it crashes on load instantly. Gives me this error which I've found no solution to so far, but I'm still looking : Uncaught TypeError: Cannot assign to read only property 'watch' of object '#' – Storm Mar 02 '18 at 19:08
  • So there must be some difference between the code you've shared in your post and the code you're attempting to run now. – thanksd Mar 02 '18 at 19:16
  • I made a new jsfiddle with maximum fidelity of my real code and it works fine. I'm suspecting the error stems from outside the vue instance, as it's bootstrapped from a bunch of vanillaJS code. Maybe something there stops me from utilizing the watch component somehow. I might have to make it work without the watch. Heres the jsfiddle : https://jsfiddle.net/6zs7fssc/ but there's not much to show since it doesnt crash. – Storm Mar 02 '18 at 19:30
  • The example in the fiddle you linked doesn't work... And it's different from the code in your post. Are you sure you're not just making a syntax error somewhere? – thanksd Mar 02 '18 at 19:47
  • I finally found out that I had another lib (draw2d.js) that registered the Object.watch globally as readonly and thus VueJs would crash when trying to modify the .watch. All is good now. – Storm Mar 06 '18 at 21:39
  • 1
    Great! If this answer was the best solution to your problem, you should accept it. That way future readers can tell what worked. – thanksd Mar 06 '18 at 21:42
  • The child should emit a copy of the value ```this.$emit('input', Object.assign({}, value))```. Otherwise if the parent is watching the value it will only get notified on the first change. – David Tinker Oct 30 '18 at 03:14
  • thanks lot @thanksd, it's really simple and elegant solution. I consumed some time to find a better way to pass data to parent as object. Your answer is what I'm looking for. – Yasin Yörük Jun 09 '21 at 12:34
  • While this solves the given problem, duplicating state in child components is a code smell and introduces a bug. If parentObject in the App changes, the child component does not rerender. A cleaner approach would be to keep the state in the parent component, to fully maximize declarative rendering. – Jasper Mar 21 '22 at 09:16
4

In #app, shoudld be parentObject, not parentValue.

In child, you had two inpyt, but you must have a single root element. In the example below I created a <div> root element for the component.

To update the parent, emit the events. This approach does not modify the parent's property in the child, so there's no breaking of the One-Way data flow.

Vue.component('child', {
  template: '#child',
  
  //The child has a prop named 'value'. v-model will automatically bind to this prop
  props: ['value']
});

new Vue({
  el: '#app',
  data: {
    parentObject: {value1: "1st Value", value2: "2nd value"}
  }
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.13/vue.js"></script>

<div id="app">
  <p>Parent value: {{parentObject}}</p>
  <child v-model="parentObject"></child>
</div>

<template id="child">
<div>
   <input type="text" v-bind:value="value.value1" v-on:input="$emit('input', {value1: $event.target.value, value2: value.value2})">
   <input type="text" v-bind:value="value.value2" v-on:input="$emit('input', {value1: value.value1, value2: $event.target.value})">
</div>
</template>

About the <input>s: you can bind each to a property of the parent's value. Then, when edited, emit an event modifying just that property (v-on:input="$emit('input', {value1: $event.target.value, value2: value.value2})) and keeping the other's value. The parent updates in consequence.

If you have many properties, you can replace in the second input, for example:

$emit('input', {value1: value.value1, value2: $event.target.value})

With

$emit('input', Object.assign({}, value, {value2: $event.target.value}))


Using a method instead of emitting directly from the template

I kept the previous demo because it is more direct and simpler to understand (less changes from your original code), but a more compact approach would be to use a method (e.g. updateValue) and reuse it in the template:

Vue.component('child', {
  template: '#child',
  
  //The child has a prop named 'value'. v-model will automatically bind to this prop
  props: ['value'],
  methods: {
    updateValue: function(propertyName, propertyValue) {
      this.$emit('input', Object.assign({}, this.value, {[propertyName]: propertyValue}))
    }
  }
});

new Vue({
  el: '#app',
  data: {
    parentObject: {value1: "1st Value", value2: "2nd value"}
  }
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.13/vue.js"></script>

<div id="app">
  <p>Parent value: {{parentObject}}</p>
  <child v-model="parentObject"></child>
</div>

<template id="child">
<div>
  <input type="text" v-bind:value="value.value1" v-on:input="updateValue('value1', $event.target.value)">
  <input type="text" v-bind:value="value.value2" v-on:input="updateValue('value2', $event.target.value)">
</div>
</template>

The demo above, as you can see, already uses Object.assign(), meaning it will handle an indefinite number of properties.

acdcjunior
  • 132,397
  • 37
  • 331
  • 304
  • 1
    While implementing your last example, to my surprise, I got it to work using simple v-model syntax, with no custom update method. Am I breaking the data flow or doing something illegal? If not, this seems much simpler. Here's a jsfiddle of how I'm implementing it : https://jsfiddle.net/bj39aqud/8/ – Storm Mar 05 '18 at 14:48
0

It is not possible. There is a closed issue asking for it. This is the shortest method I found:

Script

Vue.component('child', {
  template: `
    <div>
        <input :value="parent.foo" @change="$emit('update:parent', $event.target.value)" />
    </div>
  `,
  props: {
    parent: {
      type: Object
    }
  }
})

new Vue({
  el: '#app',
  data: () =>  ({
    parent: {foo: 'a', bar: 'b'}
  }),

  methods: {
    mutate (value) {
        this.parent.foo = value
    }
   }
})

Template

<div id="app">
  {{parent}}
  <child :parent="parent" @update:parent="mutate"></child>
</div>

https://jsfiddle.net/5k4ptmqg/475/

Julian
  • 1,380
  • 12
  • 28
  • And what if your data was dog: {size:'small', color:'brown'}, how would you pass the dog object to the child and have it update when any of its prop changes? I tried it in JSFiddle and could not get it to work. – Storm Mar 05 '18 at 13:28
  • I see. Following other answers, here is how I currently got it to work in my project : https://jsfiddle.net/bj39aqud/10/ And if v-model proves to not be enough, I would use an updateValue method like @acdcjunior mentioned – Storm Mar 05 '18 at 18:09
0

Another method without using props could be to access the components attributes $attrs in the child component.

In parent

<child v-model="parentObject" />

And in child, the value attribute generated by v-model can be accessed in template as

$attrs.value.value1

And this is reactive.

Demo HERE

Saksham
  • 9,037
  • 7
  • 45
  • 73
-1
<TradeTableItem v-for="(debtReserve, index) in debtReserves" :key="debtReserve.id" :debtReserve="debtReserve" :market="market" :id="'order_by_' + index"></TradeTableItem>

In upper step, we generated id for each rows.

And in TradeTableItem (your template where we are populating, the table rows), Write id as :id="this.id" where this.id is a part of props.

Hope this helps

Rahul Manas
  • 97
  • 1
  • 4