Vue computed property changes the data object - javascript

I have basically this structure for my data (this.terms):
{
name: 'First Category',
posts: [
{
name: 'Jim James',
tags: [
'nice', 'friendly'
]
},
{
name: 'Bob Ross',
tags: [
'nice', 'talkative'
]
}
]
},
{
name: 'Second Category',
posts: [
{
name: 'Snake Pliskin',
tags: [
'mean', 'hungry'
]
},
{
name: 'Hugo Weaving',
tags: [
'mean', 'angry'
]
}
]
}
I then output computed results so people can filter this.terms by tags.
computed: {
filteredTerms: function() {
let self = this;
let terms = this.terms; // copy original data to new var
if(this.search.tags) {
return terms.filter((term) => {
let updated_term = {}; // copy term to new empty object: This doesn't actually help or fix the problem, but I left it here to show what I've tried.
updated_term = term;
let updated_posts = term.posts.filter((post) => {
if (post.tags.includes(self.search.tags)) {
return post;
}
});
if (updated_posts.length) {
updated_term.posts = updated_posts; // now this.terms is changed even though I'm filtering a copy of it
return updated_term;
}
});
} else {
return this.terms; // should return the original, unmanipulated data
}
}
},
filteredTerms() returns categories with only the matching posts inside it. So a search for "angry" returns just "Second Category" with just "Hugo Weaving" listed.
The problem is, running the computed function changes Second Category in this.terms instead of just in the copy of it (terms) in that function. It no longer contains Snake Pliskin. I've narrowed it down to updated_term.posts = updated_posts. That line seems to also change this.terms. The only thing that I can do is reset the entire data object and start over. This is less than ideal, because it would be loading stuff all the time. I need this.terms to load initially, and remain untouched so I can revert to it after someone clears their search criterea.
I've tried using lodash versions of filter and includes (though I didn't really expect that to make a difference). I've tried using a more complicated way with for loops and .push() instead of filters.
What am I missing? Thanks for taking the time to look at this.

Try to clone the object not to reference it, you should do something like :
let terms = [];
Object.assign(terms,this.terms);

let terms = this.terms;
This does not copy an array, it just holds a reference to this.terms. The reason is because JS objects and arrays are reference types. This is a helpful video: https://www.youtube.com/watch?v=9ooYYRLdg_g
Anyways, copy the array using this.terms.slice(). If it's an object, you can use {...this.terms}.

I updated my compute function with this:
let terms = [];
for (let i = 0; i < this.terms.length; i++) {
const term = this.copyObj(this.terms[i]);
terms.push(term);
}
and made a method (this.copyObj()) so I can use it elsewhere. It looks like this:
copyObj: function (src) {
return Object.assign({}, src);
}

Related

Saving array and objects within said array

I am trying to make a simple to do app using vue.js, I want to try and save my to-dos that are set in the array so that when I reset the site, they still remain. Looking through some of the documentation I arrived at this:
data() {
return {
array: [
{id: 1, label: 'learn vuejs'},
]
}
},
methods: {
persist() {
localStorage.array = this.array;
alert('items saved')
}
},
mounted() {
if (localStorage.array && localStorage.array.id) {
this.array = localStorage.array;
this.array[id] = localStorage.array.id;
}
},
while this does save my array to localStorage, IT DOES NOT THE OBJECTS WITHIN. When I check localStorage in the console it shows :
array: "[object Object]"
anyone knows how to save the items within the array? if you do please explain it to me.
You need to store them as string. So localStorage.array = JSON.stringify(this.array), and when fetching from localStorage this.array = JSON.parse(localStorage.array);

Typescript - Complex Object merge with Dot Notation

I want to display the data in a Tree View in Angular and need to transform an array of dot-notated elements into a collection of objects with children.
This is the array I'm working with. Notice the key field in every element.
So the structure I need is for example (for the first 4 elements in the array):
const data = [
{
key: 'bs',
children: [
{
key: 'ass',
children: [
{
key: 'fixAss',
decimals: '0',
unitRef: 'unit_euro',
contextRef: 'period_2019',
value: 15542000,
children: [
{
key: 'intan',
decimals: '0',
unitRef: 'unit_euro',
contextRef: 'period_2019',
value: 8536000,
children: [
{
key: 'concessionBrands',
decimals: '0',
unitRef: 'unit_euro',
contextRef: 'period_2019',
value: 8536000,
children: [] // If there are no children in the element this can be empty or left out
}
]
},
{
key: 'tan',
decimals: '0',
unitRef: 'unit_euro',
contextRef: 'period_2019',
value: 6890000,
children: []
}
]
}
]
}
]
}
];
That means elements are combined by having a key attribute which holds the notation for that level (i.e "bs", "ass", "fixAss", ...) and then children of the next level. An element can have values of its own ("decimals", "unitRef",...) and might additionally also have children that are made up the same way. There is no restriction on the amount of levels this can have.
I have the lodash and dot object libraries in my package.json. Any help is very much appreciated.
it seems the dot-object lib has no things to work with something like "children" that you have, so it seems custom code is required to build what you expected
// balanceData got somehow
let data = [];
const getOrBuildPathObject = (path) => {
let currentLevel = data;
let obj = null;
for(let keyFragment of path.split('.')) {
obj = currentLevel.find(v => v.key == keyFragment);
if(!obj) {
obj = {key: keyFragment, children: []};
currentLevel.push(obj);
}
currentLevel = obj.children;
}
return obj;
}
balanceData.forEach((d) => {
let {key, ...data} = d;
Object.assign(getOrBuildPathObject(key), data);
})
should be something like that
I would just iterate through the array and check each key.
Split the key at the dots myArray.split('.') returns an array.
Now iterate through that array and create an Object for each element.
Like
bs.ass.fixAss
Check if a root element bs exists.
If no, create an (empty) bs Element.
Check if an ass element is a child of bs
If no, create an (empty) ass Element
Check if an (empty) fixAss Element exists.
If no, create the fixAss Element with values and add it as child to the ass Element
If yes, fill the values
If its guaranteed that the data will always be in the right order (that means bs.ass.fixAss will always be AFTER bs.ass) then you may skip the checks.
I would use a HashMap for the children (not an array), because that makes it much easier to walk through the tree
myTrees[bs].children[ass].children[fixAss]
The whole thing could be created with plain TypesScript. I do not know any library that would solve this specific problem out of the box.

Can't make filter work with Immer.js to remove nested object in array

I'm starting with Immer.js for immutability in JS and I can't find a way to remove an object in an array using the filter method. It returns the same object. BTW, I'm doing it in React with state but to make it more straightforward I made up simple snippets that reflect my problem.
const sampleArr = [
{
items: [
{ slug: "prod1", qnty: 1 },
{ slug: "prod2", qnty: 3 },
{ slug: "prod3", qnty: 2 },
],
},
];
const newState = produce(sampleArr, (draft) => {
draft[0].items.filter((item) => item.slug !== "prod1");
});
console.log(newState);
Console.log was supposed to give me the same whole array but without the first item. However, what I get is the same array without any change.
I googled it and searched on immer docs but couldn't find my answer. Immer.js docs about Array mutation => https://immerjs.github.io/immer/docs/update-patterns
Obs. To test it out on chrome dev tools you can copy-paste the immer lib (https://unpkg.com/immer#6.0.3/dist/immer.umd.production.min.js) and change produce method to immer.produce
Using destructuring for making immutable objects goes against Immert.
The way of solving this issue is by reassigning the filtered part to draft.
The solution looks like this:
const sampleArr = [
{
items: [
{ slug: "prod1", qnty: 1 },
{ slug: "prod2", qnty: 3 },
{ slug: "prod3", qnty: 2 },
],
},
];
const newState = produce(sampleArr, (draft) => {
draft[0].items = draft[0].items.filter((item) => item.slug !== "prod1");
});
You can play around in the repleit here:
https://replit.com/#GaborOttlik/stackoverflow-immer#index.js
Well, I ended up solving the problem on my own even though I'm not sure it's the right way.
What happens is that I need 2 things:
A return from the produce function
Copy the rest of the properties and add them to the new return
That said, if we write like this:
const newState = produce(sampleArr, (draft) => {
return draft[0].items.filter((item) => item.slug !== "prod1");
});
We get back the filtered items array
[
{ slug: "prod2", qnty: 3 },
{ slug: "prod3", qnty: 2 },
]
However, it's required to add back the rest of the properties. Suppose you have more data, etc. So I did like this:
const newState = produce(sampleArr, (draft) => {
draft = [
...draft.slice(0,0),
{
...draft[0],
draft[0].items.filter((item) => item.slug !== "prod1"),
}
...draft.slice(1)
];
return draft;
});
EDIT =================================================
Found out it's not required to do all that I did above. I could've done the way I did first. Was just lacking an assignment.
draft[0].items = draft[0].items.filter((item) => item.slug !== "prod1");
The problem you're running into is that Immer doesn't allow you to both modify a draft and return a completely new value from the produce. Doing so will produce the following error discussed in further detail under this question:
Error: An immer producer returned a new value and modified its draft. Either return a new value or modify the draft
As pure speculation I would guess this is intentionally disallowed because this would almost always be a bug, except for this specific use-case.
To sidestep the problem, what you can do is wrap the array you want to work with in a temporary object and then destruct that object as you retrieve the result:
const { newArray } = produce({ newArray: oldArray }, (draft) => {
// Filtering works as expected
draft.newArray = draft.newArray.filter(...);
// Modifying works as expected
if (draft.newArray.length) {
draft.newArray[0].nested.field = ...;
}
// No return statement
});

Creating a JavaScript function that filters out duplicate in-memory objects?

Okay, so I am trying to create a function that allows you to input an array of Objects and it will return an array that removed any duplicate objects that reference the same object in memory. There can be objects with the same properties, but they must be different in-memory objects. I know that objects are stored by reference in JS and this is what I have so far:
const unique = array => {
let set = new Set();
return array.map((v, index) => {
if(set.has(v.id)) {
return false
} else {
set.add(v.id);
return index;
}
}).filter(e=>e).map(e=>array[e]);
}
Any advice is appreciated, I am trying to make this with a very efficient Big-O. Cheers!
EDIT: So many awesome responses. Right now when I run the script with arbitrary object properties (similar to the answers) and I get an empty array. I am still trying to wrap my head around filtering everything out but on for objects that are referenced in memory. I am not positive how JS handles objects with the same exact key/values. Thanks again!
Simple Set will do the trick
let a = {'a':1}
let b = {'a': 1,'b': 2, }
let c = {'a':1}
let arr = [a,b,c,a,a,b,b,c];
function filterSameMemoryObject(input){
return new Set([...input])
}
console.log(...filterSameMemoryObject(arr))
I don't think you need so much of code as you're just comparing memory references you can use === --> equality and sameness .
let a = {'a':1}
console.log(a === a ) // return true for same reference
console.log( {} === {}) // return false for not same reference
I don't see a good reason to do this map-filter-map combination. You can use only filter right away:
const unique = array => {
const set = new Set();
return array.filter(v => {
if (set.has(v.id)) {
return false
} else {
set.add(v.id);
return true;
}
});
};
Also if your array contains the objects that you want to compare by reference, not by their .id, you don't even need to the filtering yourself. You could just write:
const unique = array => Array.from(new Set(array));
The idea of using a Set is nice, but a Map will work even better as then you can do it all in the constructor callback:
const unique = array => [...new Map(array.map(v => [v.id, v])).values()]
// Demo:
var data = [
{ id: 1, name: "obj1" },
{ id: 3, name: "obj3" },
{ id: 1, name: "obj1" }, // dupe
{ id: 2, name: "obj2" },
{ id: 3, name: "obj3" }, // another dupe
];
console.log(unique(data));
Addendum
You speak of items that reference the same object in memory. Such a thing does not happen when your array is initialised as a plain literal, but if you assign the same object to several array entries, then you get duplicate references, like so:
const obj = { id: 1, name: "" };
const data = [obj, obj];
This is not the same thing as:
const data = [{ id: 1, name: "" }, { id: 1, name: "" }];
In the second version you have two different references in your array.
I have assumed that you want to "catch" such duplicates as well. If you only consider duplicate what is presented in the first version (shared references), then this was asked before.

Re-render DOM if index of data elements changed

Vue.js doesnt detect that i swap 2 array elements in my data object:
data: {
list: [
'Foo',
'Bar',
'Test'
]
}
Method to swap entries:
swapIndex: function(from, to) {
var first = this.list[from];
this.list[from] = this.list[to];
this.list[to] = first;
}
JsFiddle https://jsfiddle.net/aaroniker/r11hxce8/
I want re-render the v-for loop if i swap the indexies.
Thanks!
here is the solution I came with. I created a copy of your list to modify it, and I invoked the this.$set() method on the list:
new Vue({
el: '#app',
data: {
list: [
'Foo',
'Bar',
'Test'
]
},
methods: {
swapIndex: function(from, to) {
var copy = JSON.parse(JSON.stringify(this.list))
var first = copy[from];
copy[from] = copy[to];
copy[to] = first;
this.$set(this,'list',copy)
console.log(this.list);
}
}
})
Changing value in an array with the [] operator won't let vue detect the change, replacing the array is one way to solve this, or you can use the slightly better solution of arr.splice(index, 1, newVal) suggested by the guide.

Categories