Vue: Forcing child component to react to changes in its prop (which is a dictionary) - vue.js

I am currently generating a table which lists problems encountered during the selected test using a component generated with this code:
<tr is="entry" v-for="problem in problems" :key="problem.id" v-bind:foo="problem"></tr>
Each problem corresponds to an item whose relevant information is contained within the problem dictionary and referenced in the first few columns of the table. Since the same item can have multiple problems, the same item can appear in multiple rows of the table. Now, each row features some buttons which allow you to modify the underlying item so as to fix the problems.
Whenever I modify one of those underlying items I need to modify it in all the rows, which i do by calling a function in the parent component, but modifying the data inside of the dictionary does not seem to trigger any of my watches or computes inside of the child component, which currently looks something like this:
Vue.component('entry', {
props: ['foo'],
data: function(){
//does some computations
return data
},
watch:{
foo: function(){
this.recompute_entry()
},
},
methods:{
//various methods, including:
recompute_entry: function(){
//updates the data according to changes brought to the entry
},
},
});
I have attempted to include a different prop which i could bind to an entry in a list in my parent component but, besides being pretty clunky, that didn't end up working either, which makes me think I might've gotten something wrong with my component.
Ultimately, I have relied on the fact that v-for iterates through my list in an orderly fashion, which combined with the fact that I generate no other children in my parent component means that a child component would have the same index in my component's children array as it would in my problems array. Therefore I can use this:
this.$children[problem_index].recompute_entry();
Which kind of feels hack-ish and unreliable, but actually works, for once. Is there no alternative safer method to recalculate my child components based on changes made to their props? I really feel there has to be.

I probably would need to see the exact implementation but it sounds like you need to clone your dictionary to trigger the prop change, ie:
let newProblem = Object.assign({}, this.problem);
// change any nested property
newProblem.some.value = 1
// assign back the cloned and modified dictionary
this.problem = newProblem

Related

Vue, separate out component templates

I have got a rather large template at the moment for my component. I want to separate aspects of this into it's own component.
However i'm struggling to pass data to this component. I still want to be able to manipulate the data within the child and have the data in the parent update. So for example if I pass in an object, and then use v-model within the child on a textbox, the changes should reflex within the parent.
So, i'd assume as I loop through the list of objects I would v-model them into my child component, like so:
Main.vue
<card v-for="quote in quotes" v-model="quote"></card>
And then of course accept the input within the new model:
Card.vue
export default {
props: [ 'input' ]
}
However i'm getting the following error and I can't really make sense of it.
You are binding v-model directly to a v-for iteration alias. This will not be able to modify the v-for source array because writing to the alias is like modifying a function local variable. Consider using an array of objects and use v-mode
l on an object property instead.

Chart data of object gets overwritten, but only the first object that is interacted with is affected

Here is this a code sand box proving and showcasing this issue: https://codesandbox.io/embed/ql4rm9734w?fontsize=14
When a user clicks on a button in the app's. A widget is meant to show the data of that object. The the object contains an array that is used to produce a graph. The first object's button click seems to display and function correctly. So does the second and the third. But when the first objects button is clicked again the chart data property of the object is overwritten with the chart data of the previously clicked object.
The application has been built in Vue.Js, with Highcharts, and Highcharts official Vue wrapper rendering the charts. The data is stored in a Vuex store.
The page gets populated with a button for each object. When a objects button is clicked a custom event is fired containing the object data. The object click event handler mutates the store passing the object to the store to be saved as a active marker object. The object Widget that displays the data to the user gets its data from the stores active marker object.
this process works fine for every other object that uses this system. It also only ever effects the first object clicked, all subsequent objects are unaffected and work correctly.
I have tried the following with no luck
Vue dev tools and debugging, shows the symptoms of the error but does not point to where the error takes place.
I have tried making the data property a pseudo private property that can only be accessed with setters and getters. The setter is never called.
Added second property in the class to act as a not modified storage variable for the original data given at construction time. This second property also gets modified.
When examining the store in depth, it looked like the object array in the store was not affected by the bug. However when refactored to use the object from the store directly the bug is still there.
I tried to separate out the data into a separate state property that is not related to the object in any direct way... still the same bug.
I also tried with a every small data array (15 elements) still the bug persisted.
I have even built a mini replica project in the hopes that at the smallest scale the bug does not appear and hopefully it would be a silly typo or something... but again, even this mini version of my app still shows the bug. the Mini version can be found here: https://github.com/ChadRoberts21/ChartMapBug
Built a more refined smaller example: https://codesandbox.io/embed/ql4rm9734w?fontsize=14
The code is available from https://codesandbox.io/embed/ql4rm9734w?fontsize=14
I expect that the correct chart data is always shown in the object widget and for the object to not have its data property overridden at all unless I expressly choose to do so in a Vuex mutation.
The problem occurs, because of fact that the Highcharts mutates the data, which not exactly complies the Vue conceptions. Generally, if you are able to avoid the data mutation you shouldn't do that at all. I've just answered that question directly on the highcharts-vue repository, so there you can find more specific description about why the issue occurs.
In essence (for another users searching for the answer on that question), the best way out of the problem will be to apply a spread operator when assigning a new data to series:
FooWidget.vue (chartOptions() computed property part)
series: [{
showInLegend: false,
type: "column",
color: this.foo.colour,
data: [...this.foo.data],
pointInterval: this.foo.interval,
pointStart: this.foo.startPoint,
gapSize: 4,
tooltip: {
valueDecimals: 2
},
fillColor: {
linearGradient: {
x1: 0,
y1: 0,
x2: 0,
y2: 1
}
},
threshold: null
}]
Live example: https://codesandbox.io/s/w2wyx88vxl
Best regards!
I think the problem is you are using computed for fetching data. when you pass data in click event it's actually updated foos array of the store(reference of state.foos). so when you click on test2 the data in the store of foos array is updated.
Try to console log foos array in store setActiveFoo method you can see that foos array is updated with other values.
setActiveFoo(state, payload) {
console.log("In store foos")
console.log(state.foos);
state.activeFoo = payload;
}
Try this code. I think you need to send the copy of foos array in computed it will solve the problem.
computed: {
foos() {
return JSON.parse(JSON.stringify(this.$store.getters.foos));
},
activeFoo() {
return JSON.parse(JSON.stringify(this.$store.getters.activeFoo));
}
}

Vue.js global data access with v-model

I am working on a web app where users can work on a project. The structure of the app is as follows:
Component A (app)
Component B1-Bn (header, footer, main window etc., children of A)
Component C1 (Input area; with inputs for the user to work on the project, child of main window)
Component C2 (Output area; canvas which shows the result based on inputs from C1. In the future also a "graphical" input area that syncs with C1. Child of main window)
Component D1-Dn (Single parts of the input area like tables, advanced input components etc. Child of C1)
Now the project that the user is working on consists of an object stored in Component A. Component Dn needs to write to the object in Component A and also C2 in the future.
I can't get the v-model on input components Dn to work. I tried to pass the data from A down to C1 via props / v-bind and then in Dn, I v-model the prop from C1 (which originates from A) to the input-field. I also tried to use the sync modifier without sucess.
I seem to have a lack of understanding of the vue logic. I come from a desktop background where you just define the scope of variables.
I also found that other vue apprentices have the same understanding problem but somehow the answers I found where not sufficient.
I want a "global" variable that can be edited by every component and is linked to elements in the DOM. What would be the best way to achieve this?
Declare your variable at data when creating Vue Object in your root component (Component A) like
var app = new Vue({
data: function(){
return {
showSetting: {}
}
},
})
Now you can access this showSetting variable in any component like
app.showSetting;
//change it in any component
app.showSetting = {a:1,b:2};
//or append new value to object
Object.assign({d:3},app.showSetting);
Thanks for the answers so far. I guess both of them work. I found another solution because now I fully understand how data is passed in vue:Note that objects and arrays in JavaScript are passed by reference, so if the prop is an array or object, mutating the object or array itself inside the child component will affect parent state. I will pass all data as arrays in the future, as I only want references. The only question that remains is why the programmer is not allowed to define by himself whether the data is passed by reference or not...
Source: Vue.js Guide

How can I observe any change (property added, removed or changed) in a mobx observable map?

class FilterCriteria {
#observable filter = new Map();
}
let criteria = new FilterCriteria ();
// setting up a reaction when something in the filter changes
// (property added, removed, or changed)
reaction(()=>criteria.filter, data => console.log(data.toJSON()));
criteria.filter.set('name', 'John'); // setting a new property.
I would expect the above code to print out { 'name': 'John' }, but it seems that the reaction is not running.
I suspect that I set up the reaction in the wrong way. I want to react whenever a new key is added, an existing key is removed or a key value is changed. I don't know the keys or values at compile time.
How am I supposed to do that?
UPDATE
I changed my code to
class FilterCriteria {
#observable filter = new Map();
#computed get json(){ return this.filter.toJSON(); }
}
...
reaction(()=>criteria.json, data => console.log(data));
and now it seems to work properly. The reaction sideffect is executed whenever I add, remove or change a value in the Map.
So the question is why the reaction did execute in the second but not in the first example?
UPDATE 2
I changed my code again for a second time. I reverted to almost the first version but this time instead of reacting on criteria.filter and logging data.toJSON(), i react on criteria.filter.toJSON() and I log data (toJSON is moved from the sideffect to the value being watched). This time the reaction runs normally.
class FilterCriteria {
#observable filter = new Map();
}
reaction(()=>criteria.filter.toJSON(), data => console.log(data));
Again, I don't understand why. If criteria.filter is not an observable in itself then how does the watched expression is reevaluated when something inside criteria.filter is changed?
UPDATE 4 (hope the final one) SOLUTION
According to MobX documentation, mobx reacts to any existing observable property that is read during the execution of a tracked function.
reaction side-effect executes when the observable property changes. In my example, when reacting to criteria.filter , the observable property that is read here is filter, but the filter itself never changes. It is the same map always. It is the properties of filter that change. So the reaction is never run for criteria.filter.
But when I react on criteria.filter.toJSON() or mobx.toJS(criteria.filter), the reaction is executed correctly.
So why is that? criteria.filter doesn't change, and toJSON is not an observable property. It is a function. same for mobx.toJS. It seems no properties are read here. But this is not correct. As the documentation states (but not so emphatically), the properties of criteria.filter are indeed read when toJSON or mobx.toJS is executed, because both functions create a deep clone of the map (thus iterating over every property).
Now, in the beginning, the Map did not contain any property. So how is it that newly added properties are tracked, since they did not exist (to be read) when tracking begun? This is a map's feature. Maps provide observability for not yet existing properties too.
In MobX 5 you can track not existing properties of observable objects (not class instances) too, provided that they were instatiated with observable or observable.object. Class instances don't support this.
In mobx you have two options when you want to observe changes to something that is observable. reaction and observe. Reaction allows you to specify when you want some function to be called when a specific aspect of the observable changes. This could be changes to an array length, keys, properties, really anything. observe will trigger some function any time that the observable has changed.
I suspect the reason that your reaction hasn't been triggered is because of the first function. () => criteria.filter. This will not be triggered when a key is added/removed or a value changed. Instead, it will be triggered when filter actually changes. And since filter is really a reference to the Map, it will never change, even when the Map itself changes.
Here are some examples to illustrate my point:
If you want to trigger a reaction when a key has been added or removed, you may want your function to be:
() => criteria.filter.keys()
The result of this function will be different when a key has been added or removed. Similarly, if you want to trigger a reaction for when a value has been modified, something like this should work:
() => criteria.filter.values()
So some combination of those two should be what you need to listen to changes to keys/values. Alternatively, you could use observe, which will trigger on every change and require you to check what has changed to ensure that your specific conditions have been met to warrant calling a function (ie. key/value change)
UPDATE: Here is an example that illustrates the problem
#observable map = new Map();
Lets say that the value of map in memory is 5. So when you check map === map, it is equivalent to 5 === 5 and will evaluate to true.
Now, looking at the first code snippet you posted:
reaction(() => map, data => console.log(map.toJSON()));
Every time you add/remove a key or change a value, that first function will run. And the result will be 5, since that is what we said the value in memory is for this example. It will say: the old value is 5, and the new value is 5, so there is no change. Therefore, the reaction will not run the second function.
Now the second snippet:
reaction(() => map.toJSON(), data => console.log(data));
At first the result of the function will be: {} because the Map is empty. Now lets add a key:
map.set(1, 'some value');
Now, the result of the first function will be:
{"1": "some value"}
Clearly, this value is different than {}, so something has changed, and the second function of the reaction is called.

How to organize nested props in VueJS?

I have this strange organization/architecture pickle I can't seem to figure out.
Say I have 3 components, A -> B -> C where A is a grandparent, B is a parent, and C is a child.
A passes an array of objects to B.
B loops through the array passing each object to instances of C.
Some of these objects are guaranteed not to change, while others may change.
C requires two fields in the object to be present, say object.name and object.icon. This would be specified in C's props.
My pickle is that a developer that is using component A has to look at C's props in order to know what properties to provide in the objects to B. And this problem only gets worse if I add more components between A and C.
How do I structure this?
I was thinking maybe this is where a store would come in, but I always thought stores were for maintaining state that can change. And some of the objects that A provides are guaranteed to never change.
I personally would use a shared state among components, or as you said, a store. Stores are specially useful for when you want to maintain some data between components, and when it changes in any of those, it will update in all the other components.
Simple example:
var sourceOfTruth = {
propThatChanges: {},
immutable: {}
}
var vmA = new Vue({
data: sourceOfTruth.propThatChanges
})
var vmB = new Vue({
data: sourceOfTruth.propThatChanges
})
Now whenever sourceOfTruth.propThatChanges is mutated, both vmA and vmB will update their views automatically. In the case of the objects that are guaranteed to not change, you could simply not pass it to the C components, just like sourceOfTruth.immutable. Or you could create an entire new store object available to A but not B or C
There is more info the the Vue docs, you should check it out:
http://vuejs.org/guide/application.html#State-Management