Vue - child component prop doesn't update after change on parent - javascript

I have component let's say "middle" with prop "element" from let's day "top" component - it's object and I use references to populate and update some data. This object has properties "id" and "cost". On middle component, I have select with v-model to element.id.
If user change selection, I checked some data in middle component method and set new cost. And it works fine - if I just use {{ element.cost }} to display data, everything is fine.
But I also sends this element.cost to let's say "deep" component - I use it to render custom input field and this cost is populated as value. This component use internal data to store and update model because I also use Cleave.
beforeMount () {
// model is from middle (parent)
this.value = this.model
}
and watch changes:
watch: {
value: function (newValue) {
// here some validation
this.$emit('input', this.value)
}
}
And it works fine - if I modify input, middle element also has this changes.
The problem is when I modify element.cost on "middle" level - after change in selection. Even if I use $this.set, even if I use this.$forceUpdate, input on "deep" is not updated. I tried to add watcher inside deep t monitor model, but it doesn't see any changes from parent. What's wrong?

Related

Dealing with many attributes mapping to a parent component list in VueJS

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...

Props data set as initial values for local data are not reactive

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

does ngOnChanges actually update input properties

I'm new to Angular, just a question on Lifecycle Hook Methods. I know that:
ngOnInit() is called after Angular has set the initial value for all the input
properties that the directive has declared.
ngOnChanges() is called when the value of an input property has changed and also just before the ngOnInit method is called.
let's say we have an input attribute like:
// custom directive class
#Input("pa-attr")
numOfProduct: number;
and
//template.html
<tr [pa-attr]="getProducts().length ...
and I click add button and add a new product, so the total number of products changes, so my question is:
in my case ngOnChanges() get called, ngOnInit() won't be called because ngOnInit() only get called in the first time the input property has been initialized with a value.
But how does the input property numOfProduct get updated after ngOnChanges() get called? Is it a special lifecycle method called ngUpdateInputProperties(the name won't match, but you get the idea) that does the job as:
ngUpdateInputProperties {
latestValue = ... //retrieve latest value
numOfProduct = latestValue ;
}
or such lifecycle method doesn't exist and the numOfProduct won't have the latest value, we can only get access to the latest value in ngOnChanges() method?
If you change any values in your component, it will change detection to recheck all the data bound values, then it will pass any updates to your child component.
You can test this yourself by placing a log entry inside if your getProducts() method, then tracking its value in the child component. You'll notice it gets called when updates happen in the component and you should see the updates in the child component.
Hope this helps.

How can I pass the same data multiple times from parent to component in Angular?

I have two components that communicate with each other, a regular component (parent) and a bootstrap modal component (child).
Both the parent and the child have a table with records. In the parent's table each record has a checkbox. When I select one checkbox or more and click on a button, an event is triggered that tries to populate the child's table.
The child's table gets populated and I have the ability to delete the records I wish from it. If I delete some records, close the modal (close the child) and decide to send the same data from the parent to the child again, the Input event is not triggered. However, if I choose to check different records in the parent and pass them to the child, the Input event is triggered correctly.
Here's what I have:
ParentComponent.component.html
<child-component #createNewProductRequestModal [data]="newProductRequest"></m-child-component>
ParentComponent.component.ts
private onNewProductRequest(toSend){
this.newProductRequest = toSend;
$('#createNewProductRequestModal').appendTo("body").modal('toggle');
}
ChildComponent.component.ts
#Input('data')
set data(data: any) {
if (data !== undefined || data !== "") {
this.products = data;
console.log("data");
}
}
constructor() { }
ngOnInit() { }
To test if the data is changed before I render the child's table, I log the data passed by the parent to the console. With this code, every time I execute the parent's onNewProductRequest(toSend) without changing the toSend variable, the child's modal renders but doesn't execute the Input event therefore not changing the data.
When you send the same data to the child component a second time, Angular does not register this as a change to the #Input(), as you are passing the same Object reference that you passed the first time, and Angular is just comparing the references.
Try this small change:
private onNewProductRequest(toSend){
this.newProductRequest = { ...toSend };
$('#createNewProductRequestModal').appendTo("body").modal('toggle');
}
This will mean that you pass a shallow copy of the data Object to the child, rather than just a modified version of the original Object. As this shallow copy will have a different reference to the original Object, Angular will pick it up as a change and trigger your #Input setter.
When you assign the data to the input, the first time detect the changes because the object changes, but then if yo change properties inside the object the reference and the object still be the same, so the changes doesn't fire. The solution is clone the object that you're sending via input to the child. Try changing this line:
this.newProductRequest = toSend;
For this:
this.newProductRequest = {...toSend};
You can use ngOnChanges
Definition from angular docs
A callback method that is invoked immediately after the default change detector has checked data-bound properties if at least one has changed, and before the view and content children are checked.
You have some Input on child element
#Input() nameOfInput: any;
Your child component must implement OnChanges like this:
export class ChildComponent implements OnChanges {
And in next step you want do something on every change of inputs.
ngOnChanges(changes: SimpleChanges) {
console.log(changes);
if ('nameOfInput' in changes) {
console.log('Old value: ', changes.nameOfInput.previousValue);
console.log('New value: ', changes.nameOfInput.currentValue);
}
}
Look at this example.
Example
try
in child component
import {Component, OnChanges, SimpleChanges, Input} from '#angular/core';
ngOnChanges(changes: SimpleChanges) {
for (let propName in changes) {
let change = changes[propName];
let curVal = JSON.stringify(change.currentValue);
let prevVal = JSON.stringify(change.previousValue);
console.log(curVal);
console.log(prevVal);
}
}

Vue: changing value of a data property used in v-model doesn't render the newly set value

I'm currently in the process of creating a stepper component using Vue with Vuex. Each step is a component that holds a input fields. Each step stores the values of the input fields in the Vuex store. When going to a previous step, the already available data should be loaded from the store and displayed in the respective input field.
I'm using a custom input component that implements the v-model directive correctly.
<custom-input v-model="amount"
v-bind:type="'number'"></custom-input>
"amount" is defined in the data function:
data: function () {
return {
amount: null
}
}
Now I'm trying to set the value of the v-model property when the component gets mounted.
mounted() {
this.amount = this.$store.state.fields.amount.value;
}
Through debugging tools I can see that the store holds the correct value. The same is the case for the amount data-property of the component.
I've also tried setting the property using the set method like this:
this.$set(this.$data, 'amount', this.$store.state.fields.amount.value);
But it still does not show up in the custom-input.
How do I set the data property used in v-model correctly so that it shows up in the input field?
EDIT
The custom-input component is implemented like this:
<input type="'text'" v-on:input="onInput">
onInput: function (event) {
this.$emit('input', event.target.value);
}
The problem was that I did not actually bind the value property within the custom-input component. Adding this fixed the problem:
<custom-input ... :value="value" />

Categories