Following Unexpected outcome when modifying an object in a function
I learned that i have to clone the item passed to the function before changing it and returning it, and it worked for the said example, but when i tried it in my code which was a recursive code, it didn't work, here is an example showing this:
As you can see i intend to update the property B if it exists and if it doesn't i want to create a property B and then give it last value, but for some reason this fails !, of course if i create the property B before hand (before calling it recursively), i can give the value to it, but i don't know why this is needed or why my current code doesn't work!
function addB(item) {
let newItem = { ...item };
if (newItem.B) {
newItem.B.value = "I am B";
} else {
newItem.B = {
value: "I am B"
};
}
if (newItem.children) {
newItem.children.forEach(child => {
//if you uncomment the code below, the code works!
//child.B = {};
child = addB(child);
});
}
return newItem;
}
function App() {
let parent = {
id: 0,
children: [
{
id: 1,
children: [
{
id: 3
},
{
id: 4
}
]
},
{
id: 2
}
]
};
parent = addB(parent);
console.log(parent);
}
Current output:
Expected output:
You can see this example and its result in this CodeSandBox
I think you can do the following:
function addB(item) {
const newItem = { ...item, B: { value: 'I am B' } };
if (newItem.children) {
newItem.children = newItem.children.map(addB);
}
return newItem;
}
If you want to copy the B property if it exist and only set B.value then you can do:
const newItem = { ...item, B: { ...item.B, value: 'I am B' } };
function addB(item) {
const newItem = { ...item, B: { ...item.B,value: 'I am B' } };
if (newItem.children) {
newItem.children = newItem.children.map(addB);
}
return newItem;
}
console.log(
addB({
children: [
{},
{ children: [{}, { B: { other: 2 } }] },
{ B: { something: 1 } },
],
})
);
Another way to write this is:
var addB = item => ({
...item,
B: { ...item.B, value: 'I am B' },
...(item.children
? { children: item.children.map(addB) }
: undefined),
});
Related
I have the following nested array of objects:
const data = [
{
product: {
id: "U2NlbmFyaW9Qcm9swkdWN0OjEsz",
currentValue: 34300,
},
task: {
id: "R2VuZXJpY1Byb2R1Y3Q6MTA",
name: "My Annuity",
},
instrumentDetails: [
{
instrument: {
id: "U2NlbmFyaW9JbnN0cnVtZW50OjEz",
supplier: {
id: "U3VwcGxpZXJJbnN0cnVtZW50OjUzNjQ",
supplierDetails: {
name: "Local - Class A",
},
},
currentValue: 44323,
},
assets: {
current: 1.2999270432626702,
fixed: 0.5144729302004819,
financial: 0.0723506386331588,
cash: 0.00006003594786398524,
alternative: 0.05214078143244779,
property: 0.548494862567579,
local: 0.10089348539739094,
global: 0,
},
},
],
},
{
product: {
id: "U2NlbmFyaW9Qcm9swkfefewdWN0OjEsz",
currentValue: 3435300,
},
task: {
id: "R2VuZXJpYfewfew1Byb2R1Y3Q6MTA",
name: "Living",
},
instrumentDetails: [
{
instrument: {
id: "U2NlbmFyadewwW9JbnN0cnVtZW50OjEz",
supplier: {
id: "U3VwcGxpZdwdwXJJbnN0cnVtZW50OjUzNjQ",
supplierDetails: {
name: "Local - Class B",
},
},
currentValue: 434323,
},
assets: {
current: 1.294353242,
fixed: 0.514434242004819,
financial: 0.07434286331588,
cash: 0.0000434398524,
alternative: 0.05242348143244779,
property: 0.543242567579,
local: 0.100432439739094,
global: 0,
},
},
],
},
];
The above data presents an array of products which consist of instruments that are described in instrumentDetails array. I am trying to find an instrument by supplier id and update its assets by multiplying all of the asset values by a given number.
Here is my function:
export const updateObject = (
productsArr: any,
supplierInstrumentId: string
) => {
return productsArr.map(
(product: any) => {
product.instrumentDetails.map(
(instrumentDetail: any) => {
if (
instrumentDetail.instrument.supplier.id ===
supplierInstrumentId
) {
instrumentDetail.assets.current = instrumentDetail.assets.current + 5;
instrumentDetail.assets.fixed= instrumentDetail.assets.fixed+ 5;
instrumentDetail.assets.financial= instrumentDetail.assets.financial+ 5;
instrumentDetail.assets.cash= instrumentDetail.assets.cash+ 5;
}
}
);
}
);
};
This function is giving an error :
Uncaught TypeError: Cannot assign to read only property 'current' of
object '#'
How can I deeply update the above data? Please help.
You need to return a new instrumentDetail-type object from the map function. Don't try to update the existing object.
(instrumentDetail: any) => {
const assets = instrumentDetail.instrument.supplier.id === supplierInstrumentId
? Object.fromEntries(
Object.entries(instrumentDetail.assets).map(([k, v]) => [k, v + 5])
)
:
instrumentDetail.assets;
return {
...instrumentDetail,
assets
};
}
Your product map is not returning which is why you're likely getting an undefined. I wasn't getting the typescript error which you mentioned above. This should leave the array in the state at which you intended.
const updateObject = (
productsArr: any,
supplierInstrumentId: string
) => {
return productsArr.map(
(product: any) => {
product.instrumentDetails.map(
(instrumentDetail: any) => {
if (
instrumentDetail.instrument.supplier.id ===
supplierInstrumentId
) {
instrumentDetail.assets.current += 5;
instrumentDetail.assets.fixed= instrumentDetail.assets.fixed+ 5;
instrumentDetail.assets.financial= instrumentDetail.assets.financial+ 5;
instrumentDetail.assets.cash= instrumentDetail.assets.cash+ 5;
return instrumentDetail;
}
}
);
return product;
}
);
};
If there is a value in the original object with 's' in it, it gets overidden by the value inside the array, that is nested in that object.
Where is the error.
final result (after function call):
{
value: 's2',
test: [
{ value:'s2' },
{}
]
}
expected output:
{
value: 's1',
test: [
{ value:'s2' },
{}
]
}
const sobj = {}
const simplify = (con, sobj) => {
Object.keys(con).forEach(key => {
let value = con[key];
if (typeof value === "string" && value.includes('s')) {
sobj[key] = value;
} else if (Array.isArray(value)) {
value.forEach((p, i) => {
sobj[key] = sobj[key] || [];
sobj[key][i] = {}
simplify(p, sobj[key][i]);
});
}
});
}
const data = {
value: 's1',
test: [{
value: 's2',
},
{
value: 'test'
}
]
}
simplify(data, sobj)
console.log({ data, sobj });
We have a deeply nested structure which varies each time we run the app.
{
some: {
complex: {
unknown: {
structure: {
fields: [
{ name: "group1", other: "data", currentValue: "" },
{ name: "group2", other: "another data", currentValue: "" },
]
}
}
}
}
}
We must inject, in this structure, proper value. We receive for example
{
group1: 'the proper value'
}
And we must replace the value in the proper group to obtain:
{
some: {
complex: {
unknown: {
structure: {
fields: [
{ name: "group1", other: "data", currentValue: "the proper value" },
{ name: "group2", other: "another data", currentValue: "" },
]
}
}
}
}
}
We tried to use lodash mergeWith but since we cannot know where exactly is the value we must inject and we only know the value of of of the key of the object we must inject the value in, we didn't manage to get this working.
Have a recursive function going through the object and mutating it depending on the value of what you seek.
const obj = {
some: {
complex: {
unknown: {
structure: {
fields: [{
name: 'group1',
other: 'data',
currentValue: '',
},
{
name: 'group2',
other: 'another data',
currentValue: '',
},
],
},
},
},
},
};
const toChange = {
group1: 'the proper value',
group2: 'the proper value 2',
};
// Recursive function that go replace
function lookAndReplace(config, ptr) {
// If we deal with an object look at it's keys
if (typeof ptr === 'object') {
Object.keys(ptr).forEach((x) => {
// If the keys is the one we was looking for check the value behind
if (x === config.keyToCheck) {
// We have found one occurence of what we wanted to replace
// replace the value and leave
if (ptr[x] === config.key) {
ptr[config.keyToReplace] = config.value;
}
return;
}
// Go see into the value behind the key for our data
lookAndReplace(config, ptr[x]);
});
}
// If we are dealing with an array, look for the keys
// inside each of the elements
if (ptr instanceof Array) {
ptr.forEach(x => lookAndReplace(config, x));
}
}
// For each group we look for, go and replace
Object.keys(toChange).forEach(x => lookAndReplace({
key: x,
value: toChange[x],
keyToCheck: 'name',
keyToReplace: 'currentValue',
}, obj));
console.log(obj);
/!\ Important this soluce also work with nested arrays
const obj = {
some: {
complex: {
unknown: {
structure: {
// fields is an array of array
fields: [
[{
name: 'group1',
other: 'data',
currentValue: '',
}],
[{
name: 'group2',
other: 'another data',
currentValue: '',
}],
],
},
},
},
},
};
const toChange = {
group1: 'the proper value',
group2: 'the proper value 2',
};
// Recursive function that go replace
function lookAndReplace(config, ptr) {
// If we deal with an object look at it's keys
if (typeof ptr === 'object') {
Object.keys(ptr).forEach((x) => {
// If the keys is the one we was looking for check the value behind
if (x === config.keyToCheck) {
// We have found one occurence of what we wanted to replace
// replace the value and leave
if (ptr[x] === config.key) {
ptr[config.keyToReplace] = config.value;
}
return;
}
// Go see into the value behind the key for our data
lookAndReplace(config, ptr[x]);
});
}
// If we are dealing with an array, look for the keys
// inside each of the elements
if (ptr instanceof Array) {
ptr.forEach(x => lookAndReplace(config, x));
}
}
// For each group we look for, go and replace
Object.keys(toChange).forEach(x => lookAndReplace({
key: x,
value: toChange[x],
keyToCheck: 'name',
keyToReplace: 'currentValue',
}, obj));
console.log(obj);
const obj = {
some: {
complex: {
unknown: {
structure: {
fields: [{
name: "group1",
other: "data",
currentValue: ""
},
{
name: "group2",
other: "another data",
currentValue: ""
},
]
}
}
}
}
};
const toChange = {
group1: 'the proper value',
group2: 'the proper value 2',
};
// Recursive function that go replace
function lookAndReplace({
key,
value,
keyToCheck,
keyToReplace,
}, ptr) {
// If we deal with an object
if (typeof ptr === 'object') {
Object.keys(ptr).forEach((x) => {
if (x === keyToCheck) {
// We have found one
if (ptr[x] === key) {
ptr[keyToReplace] = value;
}
} else {
lookAndReplace({
key,
value,
keyToCheck,
keyToReplace,
}, ptr[x]);
}
});
}
if (ptr instanceof Array) {
ptr.forEach(x => lookAndReplace({
key,
value,
keyToCheck,
keyToReplace,
}, x));
}
}
// For each group we look for, go and replace
Object.keys(toChange).forEach(x => lookAndReplace({
key: x,
value: toChange[x],
keyToCheck: 'name',
keyToReplace: 'currentValue',
}, obj));
console.log(obj);
A solution could be to use a recursive function like this:
object={
some: {
complex: {
unknown: {
structure: {
fields: [
{ name: "group1", other: "data", currentValue: "" },
{ name: "group2", other: "another data", currentValue: "" },
]
}
}
}
}
};
newValue={
group1: 'the proper value'
};
var inserted=false;
function search(data, newData){
if(inserted) return;
for(key in data){
if(data[key]==Object.keys(newData)[0]){
data["currentValue"]=newData[Object.keys(newData)[0]];
inserted=true;
return;
}else
search(data[key], newData);
}
}
search(object, newValue);
console.log(object);
You could do a recursive search and replace...
let theObj = {
some: {
complex: {
unknown: {
structure: {
fields: [
{ name: "group1", other: "data", currentValue: "" },
{ name: "group2", other: "another data", currentValue: "" },
]
}
}
}
}
}
function updateObj(obj, replacement) {
if(Array.isArray(obj)) {
let key = Object.keys(replacement)[0]
let itm = obj.find(i => i.name == key)
itm.data = replacement[key]
} else if(typeof obj == 'object') {
for(let i in obj) {
updateObj(obj[i], replacement)
}
}
}
updateObj(theObj, { group1: 'the proper value' })
console.log(theObj)
I am building an object from a form that is currently rendered server side. I collect all the check boxes displayed in the image below and I am trying to sort them in a way that all the check boxes under each step (1, 2, 3 etc) is a single object based on the property parentNode.
Currently the document.querySelectorAll('.checkboxes') fetches all the checkboxes in following format.
var newObj = [
{
name: 'one',
parentNode: {
id: 'stepOne'
}
},
{
name: 'two',
parentNode: {
id: 'stepTwo'
}
},
{
name: 'three',
parentNode: {
id: 'stepOne'
}
},
]
The new object should be:
var newObj = {
stepOne: [
{
name: 'one',
parentNode: {
id: 'stepOne'
}
},
{
name: 'three',
parentNode: {
id: 'stepOne'
}
},
],
stepTwo: [
{
name: 'two',
parentNode: {
id: 'stepTwo'
}
},
]
}
Usually I do something like this:
let stepOne = function(step) {
return step.parentNode.getAttribute('id') === 'stepOne';
}
let stepTwo = function(step) {
return step.parentNode.getAttribute('id') === 'stepTwo';
}
let allTheStepOnes = fetchCheckBoxes.filter(stepOne);
But filter doesn't work on dom object and this seems inefficient as well.
Proper way of doing this is a forEach loop and using associative arrays like this:
let newObject = {};
originalObject.forEach((item)=>{
let step = item.parentNode.id
if (newObj[step] === undefined) {
newObj[step] = []
}
newObj[step].push(item)
})
Using reduce we can reduce your current array into the new structure.
return newObj.reduce(function(acc, item) {
If acc[item.parentNode.id] has been defined before, retrieve this. Otherwise set it to an empty array:
acc[item.parentNode.id] = (acc[item.parentNode.id] || [])
Add the item to the array and then return it:
acc[item.parentNode.id].push(item);
return acc;
We set the accumulator as {} to start with.
Snippet to show the workings.
var newObj = [{
name: 'one',
parentNode: {
id: 'stepOne'
}
}, {
name: 'two',
parentNode: {
id: 'stepTwo'
}
}, {
name: 'three',
parentNode: {
id: 'stepOne'
}
}, ];
var newOrder = function(prevList) {
return prevList.reduce(function(acc, item) {
acc[item.parentNode.id] = (acc[item.parentNode.id] || [])
acc[item.parentNode.id].push(item);
return acc;
}, {});
}
console.log(newOrder(newObj));
This function should do the trick
function mapObj(obj) {
var result = {};
for(var i = 0; i < obj.length; i++) {
var e = obj[i];
result[e.parentNode.id] = result[e.parentNode.id] || [];
result[e.parentNode.id].push(e);
}
return result;
}
I have an array of objects similar to the following block of code:
var arr = [
{
text: 'one',
children: [
{
text: 'a',
children: [
{
text: 'something'
}
]
},
{
text: 'b'
},
{
text: 'c'
}
]
},
{
text: 'two'
},
{
text: 'three'
},
{
text: 'four'
}
];
In the above structure, I want to search a string in text property and I need to perform this search over all the children.
For example, if I search for something, the result should be an array of object in the following form:
[
{
children: [
{
children: [
{
text: 'something'
}
]
}
]
}
];
Notice all the text properties that do not match the input string something have been deleted.
I have come up with the following block of code using Array.prototype.filter. However, I can still see extra properties in the result:
function search(arr, str) {
return arr.filter(function(obj) {
if(obj.children && obj.children.length > 0) {
return search(obj.children, str);
}
if(obj.text === str) {
return true;
}
else {
delete text;
return false;
}
});
}
Here is the fiddle link: https://jsfiddle.net/Lbx2dafg/
What am I doing wrong?
I suggest to use Array#forEach, because filter returns an array, which is needed, but not practical for this purpose, because it returns all children with it.
This proposal generates a new array out of the found items, with the wantes item text and children.
The solution works iterative and recursive. It finds all occurences of the search string.
function filter(array, search) {
var result = [];
array.forEach(function (a) {
var temp = [],
o = {},
found = false;
if (a.text === search) {
o.text = a.text;
found = true;
}
if (Array.isArray(a.children)) {
temp = filter(a.children, search);
if (temp.length) {
o.children = temp;
found = true;
}
}
if (found) {
result.push(o);
}
});
return result;
}
var array = [{ text: 'one', children: [{ text: 'a', children: [{ text: 'something' }] }, { text: 'b' }, { text: 'c' }] }, { text: 'two' }, { text: 'three' }, { text: 'four' }];
console.log(filter(array, 'something'));
Your function search returns an array with object from "parent" level, that's why you "still see extra properties in the result".Secondly, this line delete text; doesn't delete an object or object property - it should be delete obj.text;. Here is solution using additional Array.map fuinction:
function search(arr, str) {
return arr.filter(function(obj) {
if (obj.text !== str) {
delete obj.text;
}
if (obj.children && obj.children.length > 0) {
return search(obj.children, str);
}
if (obj.text === str) {
return true;
} else {
delete obj.text;
return false;
}
});
}
var result = search(arr, 'something').map(function(v) { // filtering empty objects
v['children'] = v['children'].filter((obj) => Object.keys(obj).length);
return {'children':v['children'] };
});
console.log(JSON.stringify(result,0,4));
The output:
[
{
"children": [
{
"children": [
{
"text": "something"
}
]
}
]
}
]
https://jsfiddle.net/75nrmL1o/