Two-way binding in Vue.js with prop.sync and watcher - javascript

I'm having trouble understanding the two-way binding between a parent and child component. So far, what I've read seems to recommend using prop.sync in conjunction with watching the prop. E.g:
Parent:
<child-component :childProp.sync="parentData"></child-component>
Child:
<template>
<input v-model="childData" #input="$emit('update:childProp', childData);"></input>
</template>
<script>
export default {
props: ['childProp'],
data() {
childData: childProp
},
watch: {
childProp(newValue) {
this.childData = newValue;
}
}
}
</script>
My problem with this is, wouldn't this create some sort of redundancy when either the parentData or the childData is updated?
The flow would be thus (for parentData changed):
parentData changes ->
triggers watch ->
childData changes ->
triggers .sync ->
parentData updated.
I'm assuming the cycle stops at step #5 because the updated value of the parentData is the same as the old value, so parentData doesn't really change, hence step #2 watcher is not triggered.
My problem with this is, if my reasoning is right, there will be some sort of redundancy in that changes to the parentData will go to the child and reflect back to itself, and vice versa. The reflection is the redundancy. Am I right so far? Or is my understanding of this completely off?
If I'm wrong, please help me understand where I went wrong. But if I'm right, then is there another way to achieve two-way binding without this redundancy?

I suppose you can simplify a child component code like this:
<template>
<input :value="childProp" #input="$emit('update:childProp', $event);"></input>
</template>
<script>
export default {
props: ['childProp']
}
</script>

Related

VueJs force component to reload

I want a child component to reload everytime the object, which i transfer to the child as a prop, changes. I read that VueJs can not detect a change in an Object. So far so good, I came up with the following Idea:
Everytime my Object changes, I perform a change in a normal variable which I also transfer via a prop to the child. My idea was it to "force" a rerendering of the child component through the change of the normal variable. But it seems not to work and I don't understand why it doesn't work.
Parent File:
<template>
<compare-value :ocean="ocean" :update="updateComponent" v-if="ocean.length > 0"></compare-value>
</template>
<script>
import CompareValue from '#/views/compare/CompareValue'
...
components: {
CompareValue
},
...
updateComponent: 0,
...
methods: {
reloadCompnent() {
this.updateComponent += 1;
},
getData() {
this.ocean.push({
userid: userId,
data1: this.result.data_john,
data2: this.result.data_mike,
data3: this.result.data_meika,
data4: this.result.data_slivina,
})
this.reloadCompnent() //Force the reload of the component
}
}
</script>
Child File:
<template>
{{ update }}
</template>
<script>
...
props: [
'ocean',
'update'
],
...
</script>
As far as I understood, a change of a normal variable triggers the component to be reloaded but it seems I oversee something.
Setting an existing Object prop is actually reactive, and so is adding a new object to an array prop. In your example, getData() would cause compare-value to re-render without having to call reloadComponent() (demo).
I read that VueJs can not detect a change in an Object.
You're probably referring to Vue 2's change-detection caveats for objects, which calls out addition or deletion of properties on the object.
Caveat example:
export default {
data: () => ({
myObj: {
foo: 1
}
}),
mounted() {
this.myObj.foo = 2 // reactive
delete this.myObj.foo // deletion NOT reactive
this.myObj.bar = 2 // addition NOT reactive
}
}
But Vue provides a workaround, using Vue.set() (also vm.$set()) to add a new property or Vue.delete() (also vm.$delete()) to delete a property:
export default {
//...
mounted() {
this.$delete(this.myObj, 'foo') // reactive
this.$set(this.myObj, 'bar', 2) // reactive
}
}
thanks for the answers and I tested your suggested answer and I would have worked but I did something else. I just replaced the :update with :key and it worked. After this action the Component is automatically reloaded.
The solution looks exactly like the one i posted in the question just one (importatn) tiny little thing is different. See below.
<template>
<compare-value :ocean="ocean" :key="updateComponent" v-if="ocean.length > 0"></compare-value>
</template>
Thanks and hope it will help others too.
Br

Vue.js: child components are not being re-rendered

I am trying to get my head wrap around how props are passed to child components and how they are updated from there.
// parent.vue
<template>
<div>
<div v-for="elem in getCurrentArray">
<child-component :elem="elem"></child-component>
</div>
<button #click.prevent="currentIdx--">Prev</button>
<button #click.prevent="currentIdx++">Next</button>
</div>
</template>
<script>
export default {
components : {
ChildComponent
},
data(){
return {
arr : [ ["A", "B", "C" ], [ "D", "E" ]], // this would be populated from vue store
currentIdx : 0
}
},
computed : {
getCurrentArray(){
return this.arr[this.currentIdx]
}
},
}
</script>
// child.vue
<template>
<div>Prop: {{elem}} <input type="text" v-model="myinput" #blur="save" /></div>
</template>
<script>
export default {
props : [ "elem" ],
data(){
return {
myinput : this.elem
}
},
methods : {
save(){
// this.$store.dispatch("saveChildElement", { myinput }
}
},
mounted(){ console.log( this.elem + " being rendered" ) }
}
</script>
In this example, I have two sets of arrays ['A','B','C'] and ['D','E']. On page load, the first set is rendered thru child components.
Looks good. However, when I click next to go to the second set, I get this:
So, while the props are being passed correctly, the textbox input values are not updated. When I checked the console.log, it's clear that vue does not re-render child components for "D" and "E". Instead, it simply uses the existing components for "A" and "B".
Is there a way to force vue to re-render the components? If not, how can I make sure that the textbox input gets the latest prop values? Keep in mind that I want to be able to save changes to the input values via vue store.
Add a key as cool-man says, it will fix the issue.
However, your check for component re-rendering is flawed, you used the child mounted life cycle, and because you see it running only once you think the component isn't re-rendering. But this is wrong. "mounted" happens only once in a component life cycle, and because you're using the same component for ['A','B','C'] and then for ['D','E'] vue knows not to re-create the components and simply re-renders them with the new props.
Try to add the :Key prop in the v-for loop
<div v-for="elem in getCurrentArray" :key="elem">
To give Vue a hint so that it can track each node’s identity, and thus reuse and reorder existing elements, you need to provide a unique key attribute for each item:
... List Rendering - Maintaining State
Keys must be unique, if you have two or more same value in your array, for example ['A',' B', 'A'] it will create a conflict, the best way to overcome this is to add a generated unique 'ID' for each instance.
Or In alternative (a short term solution)
You can use the loop index in combination with the array value, this should give you a more or less unique key.
<div v-for="(elem, index) in getCurrentArray" :key="elem + index">
You can ask your component to re-render. (The other answers of updating the key is correct).
However, if you do want to use Vuex to manage state, I suggest you do that first as there is (essentially) a different mechanism that will do that re-render for you, and feel much easier. Namely the “computed properties” will likely solve that for you.
I remember getting stuck on this when I was starting out, and once I implemented Vuex I wished I had just done it that way, which kind of just solved the problem for me as part of its workflow. (It will likely mean you don’t need to update key, but you have that in case you have a complex situation when you still need to force a re-render).
Edit:
As #JonyB rightly pointed out, you are trying to re-render the whole component. If you think about it, what you really want to do here is not re-render the whole component, but merely update the state. Therefore, once you implement Vuex, it will likely solve this for you as it allows you to deal with state separately.
All you need to add the key to the v-for loop and it will work.
Replace
<div v-for="elem in getCurrentArray">
with
<div v-for="elem in getCurrentArray" :key="elem">
Ref: https://v2.vuejs.org/v2/guide/list.html#v-for-with-a-Component

Vue watch unexpectedly called twice

udpate #2: It is not a bug, it was my own fault. I extended the BaseComponent by a extend() method as well as putting extends: BaseComponent into the options. I thought I needed both, but extends: BaseComponent in the options seems to be enough.
So the double "extend" has apparently duplicated the watcher which lead to the strange behavior I documented in my question.
update: I found out what causes this problem: The watcher seems to be duplicated, because it's in a BaseComponent which is extended by the Component which is used in my example.
so export default BaseComponent.extend({ name: 'Component', ...}) seems to duplicate the watch object instead of "merging" it - there is now one in the BaseComponent (where it is implemented initially) and one in the Component - and of course both react to prop-updates.
This seems to be a bug IMHO.
Using vue-cli with single file components.
I am setting a prop in one component via a method:
<template>
<div>
<other-component :my-object="myObject" />
</div>
</template>
<script>
export default (Vue as VueConstructor).extend({
data() {
return {
myObject: null
}
},
methods: {
actionButtonClicked(action, ID) {
console.log('actionButtonClicked');
this.myObject = {
action: action,
ID: ID
}
}
}
});
</script>
then I am watching the prop in the other component with a watcher - but watch gets called twice on every execution of the method.
<script>
export default (Vue as VueConstructor<Vue>).extend({
/* ... */
props: {
myObject: {
type: Object,
default: null
},
watch: {
'myObject.ID'(value, oldValue) {
console.log('watcher executed');
}
}
/* ... */
});
</script>
so in the console i get the output:
actionButtonClicked
watcher executed
watcher executed
.. every time the method gets called.
I already tried all different variants of watchers - for example with deep: true + handler. but this all didn't change anything about the watcher being called twice.
In my case, I have the "watch" in a component and use the component twice on the page, so the "watch" is being registered twice.
Hopefully, it'll help somebody that has the same problem.
My watcher was duplicated because I was extending my BaseComponent in two ways:
by the extend() method of the component itself
by putting extends: BaseComponent into the options of the "outer" component
I thought you needed to use both pieces of code to extend another component, but apparently this is wrong and can lead to bad side effects.
You might want to check your component tree using a tool like the vue console or manually follow the list of imported components on your page in order to ensure no component is inadvertently imported twice.
I had a modal in the default layout, but wanted to display the modal body/main contents on a certain page (i.e without being triggered in a modal). I ended up importing both, and vue correctly treated them as separate instances. Which meant weird behavior like the modal taking two clicks to disappear, and of course, their watchers getting called twice

Update data in parent element whenever it updates in child component

Ok so let's say I have 2 components in vue, Parent.vue and Child.vue.
In Child.vue I have this
data () {
return {
childData
}
},
In Parent.vue I get the data from child using this
data(){
return{
dataFromChild : child.data().childData,
}
},
Everything good here but I have one problem, childData will get updated based on what the user does, how do I make so that dataFromChild updates whenever childData updates? I would prefer not to use event bus or vuex as it is overkill for my case.
JavaScript (and thus Vue.js) is event-driven. Are you sure using events are overkill for your case?
In an essence, you want to update the parent data, when the child data change. This change is an event. "Something" is happening, so your Parent can react to it.
I assume that you are setting the ref on the child component. I'd like to bring your attention to this section of the guide:
$refs are only populated after the component has been rendered, and they are not reactive.
I recommend you to use Vue.js event system:
https://v2.vuejs.org/v2/guide/components-custom-events.html

VueJS right way to edit prop without changing parent data

In my parent vue component I have a user object.
If I pass that user object to a child component as a prop:
<child :user="user"></child>
and in my child component I update user.name, it will get updated in the parent as well.
I want to edit the user object in child component without the changes being reflected in the user object that is in parent component.
Is there a better way to achieve this than cloning the object with: JSON.parse(JSON.stringify(obj))?
You don't have to use the JSON object.
const child = {
props:["user"],
data(){
return {
localUser: Object.assign({}, this.user)
}
}
}
Use localUser (or whatever you want to call it) inside your child.
Edit
I had modified a fiddle created for another answer to this question to demonstrate the above concept and #user3743266 asked
I'm coming to grips with this myself, and I'm finding this very
useful. Your example works well. In the child, you've created an
element in data that takes a copy of the prop, and the child works
with the copy. Interesting and useful, but... it's not clear to me
when the local copy gets updated if something else modifies the
parent. I modified your fiddle, removing the v-ifs so everything is
visible, and duplicating the edit component. If you modify name in one
component, the other is orphaned and gets no changes?
The current component looks like this:
Vue.component('edit-user', {
template: `
<div>
<input type="text" v-model="localUser.name">
<button #click="$emit('save', localUser)">Save</button>
<button #click="$emit('cancel')">Cancel</button>
</div>
`,
props: ['user'],
data() {
return {
localUser: Object.assign({}, this.user)
}
}
})
Because I made the design decision to use a local copy of the user, #user3743266 is correct, the component is not automatically updated. The property user is updated, but localUser is not. In this case, if you wanted to automatically update local data whenever the property changed, you would need a watcher.
Vue.component('edit-user', {
template: `
<div>
<input type="text" v-model="localUser.name">
<button #click="$emit('save', localUser)">Save</button>
<button #click="$emit('cancel')">Cancel</button>
</div>
`,
props: ['user'],
data() {
return {
localUser: Object.assign({}, this.user)
}
},
watch:{
user(newUser){
this.localUser = Object.assign({}, newUser)
}
}
})
Here is the updated fiddle.
This allows you full control over when/if the local data is updated or emitted. For example, you might want to check a condition before updating the local state.
watch:{
user(newUser){
if (condition)
this.localUser = Object.assign({}, newUser)
}
}
As I said elsewhere, there are times when you might want to take advantage of objects properties being mutable, but there are also times like this where you might want more control.
in the above solutions, the watcher won't trigger at first binding, only at prop change. To solve this, use immediate=true
watch: {
test: {
immediate: true,
handler(newVal, oldVal) {
console.log(newVal, oldVal)
},
},
}
you can have a data variable just with the information you want to be locally editable and load the value in the created method
data() {
return { localUserData: {name: '', (...)}
}
(...)
created() {
this.localUserData.name = this.user.name;
}
This way you keep it clear of which data you are editing. Depending on the need, you may want to have a watcher to update the localData in case the user prop changes.
According to this, children "can't" and "shouldn't" modify the data of their parents. But here you can see that if a parent passes some reactive data as a property to a child, it's pass-by-reference and the parent sees the child's changes. This is probably what you want most of the time, no? You're only modifying the data the parent has explicitly shared. If you want the child to have an independent copy of user, you could maybe do this with JSON.parse(JSON.stringify()) but beware you'll be serializing Vue-injected properties. When would you do it? Remember props are reactive, so the parent could send down a new user at any time, wiping out local changes?
Perhaps you need to tell us more about why you want the child to have it's own copy? What is the child going to do with its copy? If the child's user is derived from the parent user in some systematic way (uppercasing all text or something), have a look at computed properties.

Categories