Vue not updating computed unless forced to - javascript

I have to add extra rows forcing Vue to recompute computed prop, specifically:
var foo = this.groups;
this.groups = {};
this.groups = foo;
as can be seen in this fiddle: https://jsfiddle.net/8bqv29dg/. Without these, available_groups is not updated.
Why is that and what is the clean way to have available_groups updating with groups?
Have tried adding groups to "deep-watched", but it did not help.

Use $set to add new property for data object:
methods: {
add_group: function(key, name) {
this.$set(this.groups, key, {key, name});
},
}
Here described vue reactivity

Vue doesn't track new elements added to an object:
https://v2.vuejs.org/v2/guide/reactivity.html#Change-Detection-Caveats
One solution is to use Vue.set or reassign the object, like the example below:
new Vue({
el: "#app",
data: {
groups: {1: {key: 1, label: 'Those guys'}},
},
computed: {
available_groups: function() {
return [{value: 0, label: 'Anyone'}].concat(Object.values(this.groups));
},
},
methods: {
add_group: function(key, name) {
Vue.set(this.groups, key, {key: key, name: name})
},
}
})

Related

Why isn't the prop reactive in the dynamic component?

I have a dynamic component being injected in the slot of another one and I pass the props object to it. But when I update the data (an array) which has been assosiated with the prop (dataT: this.tableData), that prop isn't being updated inside the component.
It seems like I have a deal with two different objects but the array was passed by the reference, wasn't it?
This is the main component
<template>
<Button #click="addWindows"></Button>
<Window v-for="window in windows" :key="window.id">
<component :is="window.name" v-bind="window.props" #onDeleteRow="handleDeleteRow"></component>
</Window>
</template>
<script>
export default{
data(){
return{
windows:[],
tableData:[
{
id: '0',
name: 'dog'
},
{
id: '1',
name: 'cow'
},
{
id: '2',
name: 'cat'
}
]
}
},
methods:{
addWindows(){
this.windows = [
{
id: 0,
name: 'Component1',
props: {
dataT: this.tableData
}
},
{
id: 1,
name: 'Component2',
props: {}
}];
},
handleDeleteRow(id){
this.tableData = this.tableData.filter(r => r.id != id);
}
}
}
</script>
I expect updating dataT prop in Component1 when I modify this.tableData in the main component.
Original answer based on an earlier version of the question
If you make windows a computed property it can depend on tableData:
export default {
data() {
return {
tableData: [
{
id: '0',
name: 'dog'
},
{
id: '1',
name: 'cow'
},
{
id: '2',
name: 'cat'
}
]
}
},
computed: {
windows () {
return [
{
id: 0,
name: 'Component1',
props: {
dataT: this.tableData
}
}, {
id: 1,
name: 'Component2',
props: {}
}
]
}
}
}
If you can't make all of it a computed property, e.g. because you need to be able to modify it, then keep it as data and just use the computed property to create the array needed in your template. In that case the computed property would just be merging together different parts of the data into the correct form.
In your original code, the line dataT: this.tableData won't work because this.tableData doesn't exist yet, it'll just be undefined. There's no lazy evaluation here, it needs to resolve to the correct object at the point it hits that line.
Even if it was able to get access to the correct object it wouldn't help because in handleDeleteRow you're reassigning tableData to point to a different object. Passing 'by reference' has nothing to do with the name you use to identify the object, it refers to a reference in memory.
Incidentally, v-on also supports an object syntax, just like v-bind, so you could make onDeleteRow optional in a similar fashion.
Update based on the edited question
When you write this in addWindows:
props: {
dataT: this.tableData
}
This will assign the current value of this.tableData to dataT. That current value will be an array and as arrays are reference types any modifications made to that array will apply no matter what identifier is used to reference it.
However, this line...
this.tableData = this.tableData.filter(r => r.id != id);
... does not modify that array. Instead it assigns a totally new array to this.tableData. This will have no effect on the array referenced by dataT, which is unchanged.
There are several ways you could approach solving this, including using a computed property. However, a property getter might provide a convenient sleight-of-hand:
addWindows () {
const vm = this;
this.windows = [
{
id: 0,
name: 'Component1',
props: {
get dataT () {
return vm.tableData
}
}
},
{
id: 1,
name: 'Component2',
props: {}
}
];
}
This will always evaluate to the current value of tableData. Vue's reactivity should be fine with this extra indirection, it just sees it as equivalent to accessing tableData directly.
TL;DR
The issue is with your binding. Use the following:
<component
:is="window.name"
:dataT="window.props.dataT"
#onDeleteRow="handleDeleteRow">
</component>
Explanation
the v-bind attribute specifies what prop is bound to what value (or reference). In your case, you didn't specify what values you're binding to what props, thus the component props weren't bound as expected.

how do I add an item on array of Objects on setState in React?

I am creating arrays of objects and storing it on variables like this:
const nameOption = nameOptions(listHotels);
const estadoOption = stateOptions(listHotels);
const cityOption = cityOptions(listHotels);
my state is currently like this:
selectFilter: [
{ id: 1, type: 'Name'},
{ id: 1, type: 'Estado'},
{ id: 1, type: 'Cidade'},
],
I want to add these variables in a property called "options", like so:
selectFilter: [
{ id: 1, type: 'Name', options: nameOption},
{ id: 1, type: 'Estado', options: estadoOption},
{ id: 1, type: 'Cidade', options: cityOption},
],
how do I do it using the immutable react way?
First store the values to be inserted into a plain object, where the properties match the type values of selectFilter :
const options = {
Name: nameOptions(listHotels),
Estado: stateOptions(listHotels),
Cidade: cityOptions(listHotels)
}
Then merge that with selectFilter into the extended version of it:
this.setState(prevState => ({
selectFilter: prevState.selectFilter.map(filter =>
({...filter, options: options[filter.type]})
)
}));
NB: there is probably a more efficient way to build the options object, since you seem to iterate listHotels for each property. This could probably be done in one sweep using reduce. But without details about what these functions (nameOptions, stateOptions, ...) do there is not much I can offer for that. Look into calling listHotels.reduce.
You can use Object.assign() to duplicate your state into an new object. This object is now mutable. Once done modifying it, you then replace your entire state with the new version. Here is an example of how that could be done.
handleChange= () => {
let mutableState = Object.assign({}, this.state);
mutableState.thingToChange = foo;
this.setState(mutableState);
};
this.setState(({selectFilter}) => (
[
{...selectFilter[0], options: nameOptions},
{...selectFilter[1], options: estadoOptions},
{...selectFilter[2], options: cityOptions},
]
);

Prevent prop from overwriting the data

I'm new to vue.js and struggling with the following scenario.
I send an array filled with objects via props to my router-view.
Inside one of my router-view components I use this array in multiple functions, reference it with 'this.data' and safe it inside the functions in a new variable so I don't overwrite the actual prop data.
However the functions overwrite the original prop data and manipulate the data of the prop.
Here is an abstract example of my question:
App.vue
<template>
<div>
<router-view :data='data'></router-view>
</div>
</template>
<script>
export default {
data: function() {
return {
data: [],
};
},
created: function() {
this.getData();
},
methods: {
getData: function() {
this.data = // array of objects
},
}
route component:
<script>
export default {
props: {
data: Array,
},
data: function() {
return {
newData1 = [],
newData2 = [],
}
}
created: function() {
this.useData1();
this.useData2();
},
methods: {
useData1: function() {
let localData = this.data;
// do something with 'localData'
this.newData1 = localData;
}
useData2: function() {
let localData = this.data;
// do something with 'localData'
this.newData2 = localData;
}
}
}
</script>
The 'localData' in useData2 is manipulated from changes in useData1, whereby I don't overwrite the data prop.
Why do I overwrite the prop and how can i prevent it?
The problem you're experiencing a side effect of copying this.data by reference, rather than value.
The solution is to use a technique commonly referred to as cloning. Arrays can typically be cloned using spread syntax or Array.from().
See below for a practical example.
// Methods.
methods: {
// Use Data 1.
useData1: function() {
this.newData1 = [...this.data]
},
// Use Data 2.
useData2: function() {
this.newData2 = Array.from(this.data)
}
}
#Arman Charan is right on his answer. Object and arrays are not primitive types but reference.
There is an awesome video explanation here => JavaScript - Reference vs Primitive Values/ Types
So for reference types you first have to clone it on another variable and later modify this variable without the changes affecting the original data.
However for nested arrays and objects in high level the spead and Array.from will not work.
If you are using Lodash you can use _.cloneDeep() to clone an array or an object safely.
I like functional programming and I use Lodash which I strongly recommend.
So you can do:
let original_reference_type = [{ id:1 }, { id: 2 }]
let clone_original = _.cloneDeep(original_reference_type)
clone_original[0].id = "updated"
console.log(original_reference_type) //[{ id:1 }, { id: 2 }] => will not change
console.log(clone_original) // [{ id: "updated" }, { id: 2 }]
Suggestion: For simple arrays and objects use:
Objects:
let clone_original_data = {...original_data} or
let clone_original_data = Object.assign({}, original_data)
Arrays:
let clone_original_data = [...original_data] or
let clonse_original_data = original_data.slice()
For complex and high nested arrays or Objects go with Lodash's _.cloneDeep()
I think this is most readable, "declarative" way:
First, install lodash npm i lodash. Then import desired function, not the whole library, and initialize your data with array from props.
<script>
import cloneDeep from 'lodash/cloneDeep'
export default {
props: {
data: Array
},
data () {
return {
// initialize once / non reactive
newData1: cloneDeep(this.data),
newData2: cloneDeep(this.data)
}
}
}
</script>

Manipulating the data value in Vue.js

How can I set the value of name to the property personOne in object? So that name will have the value of Alex.
var app = new Vue({
el: '#app',
data: {
name: '',
object: { "personOne": "Alex", "personTwo": "Jack"}
}
})
From within the Vue object you write
this.name = 'Alex' and outside you write app.name = 'Alex'
app.someDataField will change the data property called someDataField
You can use one of the life-cycle hooks like created created or mounted for setting initial data, loading data from API, etc, like following:
var app = new Vue({
el: '#app',
data: {
name: '',
object: { "personOne": "Alex", "personTwo": "Jack"}
},
methods: {
setName (name) {
this.name = name
}
},
mounted () {
this.setName(this.object.personOne)
},
})

Updating KnockoutJS associative observableArray values

This should be really simple.
I have an associative observable array with a name and a boolean value.
this.items = ko.observableArray([
{ name: "name1", boolVal: true },
{ name: "name2", boolVal: true },
]);
Then a simple function to change boolVal.
this.changeValue = function (item) {
item.boolVal = false;
};
When I call the changeValue function, boolVal does change (see console.log(data) in my jsfiddle) but the view doesn't update. The value on the screen remains "true". I must be making an incorrect assumption regarding how KnockoutJS works.
JS Fiddle Link
In order to the KO update UI you need to have observable properties:
this.items = ko.observableArray([
{ name: "name1", boolVal: ko.observable(true) },
{ name: "name2", boolVal: ko.observable(true) },
]);
And set it with:
this.changeValue = function (item) {
item.boolVal(false);
};
The ko.observableArray only tracks item addition and removal. So it won't notify the UI if one of its items changed. For that you need to have ko.observable on the items.
Demo JSFiddle.

Categories

Resources