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.
Related
I'm wondering if I'm doing this the best way possible; I've built a component which accepts a list of items as a prop, but then I need to mutate that list internally in the component, so I followed the VueJs docs to provide the prop value as a data property's initial value, and then using the data property when I need to make any changes to the data.
export default {
props: {
items: {
type: Array,
default: () => {
return []
}
}
},
data() {
return {
itemList: this.items
}
}
}
I noticed however that this causes the itemList property to stick to the initial value, and won't update if the parent component provides a new items value to this component, this makes sense as the component was already rendered and thus it doesn't need to reevaluate all it's data properties; so to fix this I added a watcher to watch for any changes on items:
watch: {
items() {
this.itemList = this.items;
}
}
I found this other question which had a similar issue and the accepted answer did just what I just did here; however I'd like to know if this is an expected and reliable method of fixing this issue or if there are other ways to do this?
The data property uses the prop just for initialization. It won't react to prop value changes after that.
A computed property will, though. It's another way to do it, but since you're not actually doing any "computing" on the prop value, a watcher with a function that changes the data property seems more appropriate to me.
A regular watcher is shallow, though. Your suggestion shouldn't work if the parent element just mutated the array by adding or removing elements. It should only react to a reassignment of the array (I guess that's what your parent element did... replace the prop value entirely).
If you need the watcher to react to mutations to the prop as well, then you might want to create a deep watcher:
watch: {
items: {
handler(newValue) {
this.itemList = JSON.parse(JSON.stringify(newValue)); //deep array copy to prevent array mutations made by the child component from affecting the parent's array. Keep in mind this deep-copy technique using JSON has caveats.
},
deep: true
}
}
But that can become quite expensive if your array prop grows big.
Also, it seems to me you intend for the state of your component ($data.itemList) to be changed both by the component itself and by its parent *at the same time*. This can get troublesome (racing conditions and whatnot), so be careful.
I've got a list of components where I'd like them all to be editable / replicate state to the parent component.
The list component exists as:
Vue.component("shortcuts", {
props: {
shortcuts: Array
},
template: '...<shortcut-entry v-for="(shortcut, index) in shortcuts" v-bind:key="index" v-bind="shortcut" #remove="remove(index)"></shortcut-entry>...'
})
It's used with a model like this:
<shortcuts v-bind:shortcuts.sync="shortcuts"></shortcuts>
Now each shortcut-entry component will contain lots of values which I would like to be propagated back to the top level list of objects:
Vue.component("shortcut-entry", {
props: {
mod_ctrl: Boolean,
mod_alt: Boolean,
mod_shift: Boolean,
keypress: String,
action: String,
...
},
Each of those properties exists as a separate checkbox / input on the page with (for example) <input ... v-model="action">. The way I understand it, I could wire the update events back to the parent component and do replacements there... but that sounds like a lot of boilerplate code.
Can I somehow propagate any modifications for those props back to the parent component automatically? (avoiding the "Avoid mutating a prop directly" warning)
It seems to work as I expect if I move every prop I currently have into another level (so I have props: {options: Object}, v-bind it with .sync and assign everything there), but I'm looking into some more explicit solution which actually declares the relevant options ahead of time.
You can use sync modifier with the props object notation together. Instead of v-bind="shortcut" use v-bind.sync="shortcut"
This way shortcut component can declare all props (instead of just options object) and still be able to notify parent about mutation
But you need to change how you bind your inputs. Instead of <input ... v-model="action"> you need to <input ... v-bind:value="action" v-on:input="$emit('update:action', $event.target.value)">. Problem with this is that different <input> types use different names for value and change event
To work around it and keep using v-model you can declare computed for each prop and use v-model="actionComputed"
computed: {
actionComputed: {
get: function() { return this.action },
set: function(value) { this.$emit('update:action', value) }
}
}
So the result is again lot of boilerplate...
TL:DR
If you want all props declared ahead of time (instead of single prop of type object), some boilerplate is necessary. It can be reduced by generating computed props for v-model on the fly (same way Vuex helpers generate computed to access and update Vuex state) but I don't think it is worth the effort. Just pass an object and let the <input> components mutate it's properties...
In order to properly copy props to local data and manipulate them in your component you need to use Computed props.
What do you do when you want to set the default value of a computed prop to be based on a prop but to also be able to override its' value manually without resetting the entire computed property?
props: {
thing: {
type: Object,
required: false,
default: null,
},
},
computed: {
form() {
return {
name: this.thing.name,
someLocalThing: 'George Harrington',
};
},
}
and then
<v-text-field v-model="form.name">
<v-text-field v-model="someLocalThing">
The problem is, changing someLocalThing, overrides/resets form.name (the computed prop is re-evaluated) so we lose the changes we had just previously done.
edit: this is an exact reproduction link : https://codesandbox.io/s/vuetify-2x-scope-issues-ct0hu
You could do something like this
props: {
thing: {
type: Object,
required: false,
default: null,
},
},
data () {
return {
name: this.thing.name,
someLocalThing: 'George Harrington'
}
}
Now you can modify the data inside of component, and the props are still the same.
If you want to apply these changes directly to parent component as well, you will have to emit a function with the updated data.
I had the same issue today. In my use case, users are opening a dialog to edit an object, My solution was to fill the form on the button press to open the dialog. This seems to be a Vuetify issue. I have searched the Vuetify Github repo, but I have not been able to find this issue.
Regardless, here is my implementation (trimmed down for brevity).
#click="fillForm()" calls a function to fill the v-textarea s
<v-dialog v-model="dialog" persistent max-width="600px">
<template v-slot:activator="{ on }">
<v-btn color="primary" v-on="on" #click="fillForm()" width="100%">Edit RFQ</v-btn>
</template>
...
</v-dialog>
script
export default {
props: ['rfq'],
data(){
return {
title: undefined,
notes: undefined,
companiesRequested: undefined,
}
},
methods: {
fillForm() {
this.title = this.title ? this.title : this.rfq.title;
this.notes = this.notes ? this.notes : this.rfq.notes;
this.companiesRequested = this.companiesRequested ? this.companiesRequested : this.rfq.company_requested_ids;
},
}
}
In order to properly copy props to local data and manipulate them
First, in your example you have no local data. What your computed property is is some temporary object, which can be recreated any time something changes. Don't do that.
If the prop is really just some initial value, you can introduce property in data() and initialize it right there from prop
If you are passing the prop to component in order to change its value, you probably want that changed value passed back to your parent. In that case you don't need to copy anything. Just props down, events up (either with v-model, .sync or just handling events manually)
In the case Object is passed by prop, you can also directly mutate object's properties - as long as you don't change prop itself (swap it for different object), everything is fine - Vue will not throw any warning and your parent state will be mutated directly
UPDATE
Now I have better understanding of use case. Question should be more like "Best way to pass data into a modal popup dialog for editing item in the list/table"
Dialog is modal
Dialog can be shown for specific item in a table by clicking button in it's row
Dialog is cancelable (unless the user uses Save button, any changes made in the dialog should be discarded)
In this particular case I don't recommend using props to pass the data. Props are good for passing reactive values "down". Here you don't need or want dialog to react for data changes. And you also don't want use props for 1-time initialization. What you want is to copy data repeatedly at particular time (opening the dialog).
In component with table (row rendering):
<td>
<v-btn rounded #click="openDialog(item)">open details</v-btn>
</td>
Place dialog component outside the table:
<person-form #update="onUpdate" ref="dialog"></person-form>
In methods:
openDialog(item) {
this.$refs.dialog.openDialog(item);
},
onUpdate(item) {
// replace old item in your data with new item
}
In dialog component:
openDialog(item) {
this.form = { ...item }; // copy into data for editing
// show dialog....
},
// Save button handler
onSave() {
this.$emit("update", this.form);
// hide dialog...
}
Demo
If I'm passing (a reference to) an Object as a prop is it OK to mutate values in the prop?
I'm developing a web app which will require a lot of values to be passed to a component, and I'm trying to find the best way of passing the values to the component and back to the parent.
From everything I've read mutating a prop is the wrong way to do things, because next time the component is updated the values are passed back to the child component overwriting the mutations. But only the reference to the object is passed so any mutations to the values in the object prop happen to the original object in the parent component. Also Vuejs does not complain about mutating props when this happens.
const subComponent = {
name: "subComponent",
template: `
<div>
Sub Component Input
<input type="text" v-model="objectProp.val1"></input>
</div>`,
props: {
objectProp: {
required: false,
default: () => {return {val1: "carrot"}}
}
}
}
const aComponent = {
name: "aComponent",
template: `
<div>
val1: {{mainObject.val1}}
val2: {{mainObject.val2}}
<sub-component :objectProp="mainObject"></sub-component>
</div>`,
data: function() {
return{
mainObject: {
val1: "foo",
val2: "bar"
}
}
},
components: {
subComponent
}
}
new Vue({
el: "#app",
components: {
aComponent
}
})
Here is a JSFiddle showing an object prop being mutated.
JSFiddle
Is mutating a prop bad practice?
Yes absolutely. In more complex applications it is very easy to lose track of where/what/why is mutated
What is the right way to handle state across different components?
In small projects you can really do whatever you want, because you will most likely be able to follow the logic - even after not looking at the project for a year. The possibilities include:
Mutating Props (ugly but will work)
Using Events to mutate state in parent components; take a look at the EventBus (better)
Use global shared state; look at Vuex (best, but a little more boilerplate)
In big projects, however, you should absolutely use Vuex. It is a module for Vue that adds global shared state to your app, which you can access and mutate from all places in your app.
I think I understand what you are trying to do, You want to pass data to a child component, mutate it and then give that data back to the parent component.
You never want to mutate the props data given to the child component, however you CAN mutate a local state of the data, which could be an exact clone of the prop.
You can do this in many ways, I normally use a computed property as suggested in the Vue documentation:
https://v2.vuejs.org/v2/guide/components-props.html
in the computed return, just return the data coming in from the property.
I have a Prop in my component that is a User object, I then have this function:
onChange: function(event){
this.$v.$touch();
if (!this.$v.$invalid) {
this.$axios.put('update',{
code:this.user.code,
col:event.target.name,
val:event.target.value
}).then(response => {
this.user[event.target.name]=event.target.value
});
}
}
I can see in the Vue console debugger that the correct attribute has been updated, this attribute exists when the component is created but the template where I reference it does not refresh:
<p>Hi {{user.nickname}}, you can edit your details here:</p>
This is all within the same component so I'm not navigating to child or parent. I'm sure props have been reactive elsewhere in my code?
Ok, it seems this is intended behaviour. According to the documentation
https://v2.vuejs.org/v2/guide/components-props.html in the scenario that I have it should be handled as:
The prop is used to pass in an initial value; the child component wants to use it as a local data property afterwards. In this case,
it’s best to define a local data property that uses the prop as its
initial value:
props: ['initialCounter'],
data: function () {
return {
counter: this.initialCounter
}
}
Usually components should be reactive to Props, though i have had experiences where it was non-reactive so i added the prop to a watcher and put the functional call there.
props: ["myProp"],
watch:{
myProp(){
// ... your functions here
}
}