How to make sure Array has object items only with unique keys - javascript

I'm creating an online store. The product has attributes (in this case, iMac computer has attributes like: capacity, usb features and digital keyboard.)
I store the attributes in my state.
The problem is, every time I switch from, for example, 256GB capacity to 512GB capacity, it adds an entire new object with {capacity:"512GB"} to the array.
How do I configure my handler function (code below) so that it conditionally checks if the object in an array already has 'Capacity' key, and updates that to a new selection, instead of adding another object? I tried everything, I'm desperate
the handler receives object with key-value pair, and the key (as label) itself, and type. In this case, type can be ignored.
const handleAttributeChange = (object, type, label) => {
if (type == "text") {
this.setState({
selectedAttributes: {
id: id,
text: [...this.state.selectedAttributes.text, object],
swatch: this.state.selectedAttributes.swatch,
},
});
} else if ((type = "swatch")) {
this.setState({
selectedAttributes: {
id: id,
text: this.state.selectedAttributes.text,
swatch: [...this.state.selectedAttributes.swatch, object],
},
});
}
};

Instead of an array, you can take objects with diff key values. If keys are unique. Later u can convert it into the list of values.
let obj = {};
const update = (o) => {
obj = { ...obj, ...o };
};
/* In case you know key, value */
const update2 = (key, value) => {
obj[key] = value;
};
console.log(obj);
update({ a: 1 });
update({ b: 2 });
console.log(obj); // { a: 1, b: 2 }
update({ a: 3 });
console.log(obj); // { a: 3, b: 2 }
console.log(Object.entries(obj).map(([key, value]) => ({ [key]: value })));
//[ { a: 3 }, { b: 2 } ]

Related

How to add a property to a dynamically changing object from an API?

I hope someone can help me with my problem! I didn't find the right thing through the search and maybe someone can give me a hint.
I am calling an API that returns an object that in turn contains nested objects. In these nested objects there are two properties "value" and "scale". I want to divide these two properties and write them as properties in the same object.
The data I get from the API is dynamic, which means it is constantly changing.
Example:
// call api
const apiCall = callApi(...);
// return object
console.log(apiCall);
{
id: '3454353458764389759834534534',
json_data: {
persons: {
de: {
name: 'Bob',
data: {
scale: 100,
value: 2459,
},
},
be: {
name: 'Alice',
data: {
scale: 10000,
value: 1459,
},
},
},
url: 'https://stackoverflow.com/',
timestamp: '2021-10-23T12:00:11+00:00',
disclaimer: 'Some bla bla',
},
}
// targed object
const objTarged = {
id: '3454353458764389759834534534',
json_data: {
persons: {
de: {
name: 'Bob',
data: {
scale: 100,
value: 2459,
result: 24.59 // value / scale = result
},
},
be: {
name: 'Alice',
data: {
scale: 10000,
value: 1459,
result: 0.1459 // value / scale = result
},
},
},
url: 'https://stackoverflow.com/',
timestamp: '2021-10-23T12:00:11+00:00',
disclaimer: 'Some bla bla',
},
};
My thoughts:
do I need to map the object into a new object?
how can I do this if the source object is constantly changing (Object.values?)
How can I write the result of Value / Scale as a new property in the same object each time I call the API?
Big thanks in advance :)
It might be helpful to decompose the problem into first finding the nested objects with the keys you're interested in. Having done that, it will be easy to augment those objects with the desired calculation.
Below is a sort of generic function that finds a nested object based on it having a particular key. With that, fixMyApiData writes itself...
// return an array of objects that are nested in the passed object which contain the passed key
function objectsContainingKey(object, key) {
let results = [];
Object.keys(object).forEach(k => {
if (k === key) results.push(object);
if (object[k] && typeof object[k] === 'object')
results = results.concat(objectsContainingKey(object[k], key));
});
return results;
}
// find the nested objects we care about and augment them with the value/scale calculation
function fixMyApiData(apiData) {
objectsContainingKey(apiData, 'scale').forEach(data => {
if (data.value) data.result = data.value / data.scale;
})
}
let apiData = {
id: '3454353458764389759834534534',
json_data: {
persons: {
de: {
name: 'Bob',
data: {
scale: 100,
value: 2459,
},
},
be: {
name: 'Alice',
data: {
scale: 10000,
value: 1459,
},
},
},
url: 'https://stackoverflow.com/',
timestamp: '2021-10-23T12:00:11+00:00',
disclaimer: 'Some bla bla',
},
};
fixMyApiData(apiData);
console.log(apiData);
I would create a mapValues() function that takes an object, and creates a new object by passing each of the object's values in a transforming function.
Whenever the api call returns a new object, we recreate the new object with the result property according to the structure.
How does the mapValues function works?
Whenever an object (or array) is passed to mapValues, it's converted to an array of [key, value] pairs. The pairs are then mapped to new [key, pair] entries by applying transformFn to the value. The transform array of pairs is then converted back to an using Object.fromEntries().
const mapValues = (transformFn, obj) => Object.fromEntries(
Object.entries(obj)
.map(([key, value]) => [key, transformFn(value)])
)
const apiCall = {"persons":{"de":{"name":"Bob","scale":100,"value":2459},"be":{"name":"Alice","scale":10000,"value":1459}}}
const result = mapValues(
val => mapValues(v => ({
...v,
result: v.value / v.scale,
}), val),
apiCall
)
console.log(result)
If you have multiple nested levels with properties you don't want to transform, we can also pass the key to the transformFn for a more granular change. Now we can create a recursive function to traverse the tree, and only update objects which have a specific key.
const mapValues = (transformFn, obj) => Object.fromEntries(
Object.entries(obj)
.map(([key, value]) => [key, transformFn(value, key)])
)
const fn = obj => mapValues(
(val, key) => {
// if the key is data create a new object with a result property
if(key === 'data') return ({
...val,
result: val.value / val.scale,
})
// if it's object pass it to the recursive function
if(typeof val === 'object') return fn(val)
return val
},
obj
)
const apiCall = {"id":"3454353458764389759834534534","json_data":{"persons":{"de":{"name":"Bob","data":{"scale":100,"value":2459}},"be":{"name":"Alice","data":{"scale":10000,"value":1459}}},"url":"https://stackoverflow.com/","timestamp":"2021-10-23T12:00:11+00:00","disclaimer":"Some bla bla"}}
const result = fn(apiCall)
console.log(result)

Alternative to Object.fromEntries?

I receive an object like this:
this.data = {
O: {
id: 0,
name: value1,
organization: organization1,
...,
},
1: {
id: 1,
name: value1,
organization: organization1,
...,
},
2: {
id: 2,
name: value2,
organization: organization2,
...,
},
...
}
I then filter by id and remove the Object which id matches the id I receive from the store like so:
filterOutDeleted(ids: any[], data: object,) {
const remainingItems = Object.fromEntries(Object.entries(data)
.filter(([, item]) => !ids.some(id => id === item.id)));
const rows = Object.keys(remainingItems).map((item) => remainingItems[item]);
return rows;
}
Unfortunately, I'm getting an error when building stating Property 'fromEntries' does not exist on type 'ObjectConstructor' and I am unable to make changes in the tsconfig file at this point. Is there an alternative for fromEntries for this case? Any help is much appreciated!
Create the object outside instead, and for every entry that passes the test, assign it to the object manually.
Also note that you can decrease the computational complexity by constructing a Set of the ids in advance:
const filterOutDeleted = (ids: any[], data: object) => {
const idsSet = new Set(ids);
const newObj = {};
for (const [key, val] of Object.entries(data)) {
if (!idsSet.has(val.id)) {
newObj[key] = val;
}
}
return newObj;
};

ReactJS: How to access and update nested state object with dynamic key?

Suppose I have a component with state defined as follows:
this.state = {
apple:{
a:1,
b:2,
},
mango:{
banana : {
a:1,
b:2,
}
}
}
If I wanted to update the value of a nested object in my state, I could do so with hard coded keys as shown below:
cost temp = { ...this.state['mango'] }
temp['banana']['a'] = 2;
this.setState({mango:temp});
How would I update a nested value in my state object dynamically key? For example, if I had a JSON path in either dot or array notation, how could I update my component state?
One way to achieve this would be to acquire the nested object that is the parent of the field that your path is targeting via Array#reduce:
const nestedObject = path
.slice(0, -1)
.reduce((object, part) => (object === undefined ? undefined : object[part]), { ...state })
And then update the last key/value of nestedObject by via the last key of your path:
/* Get last part of path, and update nestedObject's value for this key, to 2 */
const [pathTail] = path.slice(-1);
nestedObject[pathTail] = 2;
The following snippet shows these two ideas together:
/* Path of nested field to update, in array notation */
const path = ['mango', 'banana', 'a'];
/* Components state */
const state = {
apple: {
a: 1,
b: 2,
},
mango: {
banana: {
a: 1,
b: 2,
}
}
};
const stateClone = { ...state };
/* Aquire the parent object (ie banana) of the target field (ie a) */
const nestedObject = path
.slice(0, -1)
.reduce((object, part) => (object === undefined ? undefined : object[part]), stateClone)
if (nestedObject !== undefined) {
/* Obtain last key in path */
const [pathTail] = path.slice(-1);
/* Update value of last key on target object to new value */
nestedObject[pathTail] = 2;
}
/* Display updated state */
console.log('Updated state:', stateClone)
/* Call this.setState: */
// this.setState(stateClone);
Update
Here is some extra detail outlining how the reduce() part of the answer works:
path
/* slice obtains ['mango', 'banana'], seeing -1 clips last item */
.slice(0, -1)
/* reduce iterates through each part of array ['mango', 'banana']
where at each iteration we fetch the corresponding nested object
of the { ...state } object that's passed in */
.reduce((object, part) => {
/* At iteration 1:
object has two keys, 'apple' and 'mango'
part is 'mango'
object is defined, so return object['mango'] for first iteration
At iteration 2:
object passed from last iteration has one key, 'banana'
part is 'banana'
object is defined, so return object['banana'] for second iteration
Reduce complete:
we return object['banana'], which is the same as state['mango']['banana']
*/
if(object === undefined) { return undefined; }
return object[part]
}, stateClone)
Having:
const [formState, setFormState] = useState(
{
id:1,
name:'Name',
innerObjectName: {
propA: 'Something',
propB: 'Another thing',
}
});
Maybe you're looking for something like this:
const handleComplexInputChange = (evt, object) => {
setFormState({
...formState,
[object] : {
...formState[object],
[evt.target.name]: evt.target.value,
}
})
}
And from your component you should call it like this:
onChange={(e) => {
handleComplexInputChange(e, "innerObjectName");
}}

Dynamically setState() with key nested state

I have a method in a component. I want to dynamically setState with a key in a nested array of objects.
method = (name, value) => {
console.log(name)
//a //value is 1
//b //value is 2
//c //value is 3
this.setState({ [name]:value })
}
when its not nested, it dynamically changes state successfully. However when its nested
method = (name, value) => {
this.setState({
ArrayOfObjects:[{
[name] : value
}]
}
My state becomes
state = {
ArrayOfObjects: [{
c: 3
}]
}
I want
state = {
ArrayOfObjects: [{
a: 1,
b: 2,
c: 3
}]
What's wrong?
You could just push an element to the current ArrayOfObjects.
ArrayOfObjects = this.state.ArrayOfObjects;
ArrayOfObjects.push({[name] : value});
this.setState({
ArrayOfObjects
});
Or using the spread operator:
this.setState({
ArrayOfObjects: [
...this.state.ArrayOfObjects,
{[name] : value}
]
});
Assuming that ArrayOfObjects is an always an array with single object and that you want to merge name/value into that object:
method(name, value) {
// make a copy
const ArrayOfObjects = [...this.state.ArrayOfObjects];
// merge properties and set dynamic value
ArrayOfObjects[0] = { ...ArrayOfObjects[0], [name]: value };
this.setState({
ArrayOfObjects
});
}
Here is an example in action.
Hopefully that helps!

Nest/Group an array of objects by attribute, ignoring null/undefined and using additional property as label

I'm trying to nest-group an array of objects.
The function provided by this gist almost works as intended and uses lodash as basis:
https://gist.github.com/joyrexus/9837596
const _ = require('lodash');
function nest(seq, keys) {
if (!keys.length) return seq;
let [first, ...rest] = keys;
return _.mapValues(_.groupBy(seq, first), value => nest(value, rest));
}
This recursively,
However, there are two problems I face.
if a parameter is set to null or undefined, it is used as a group, instead the
an optional object attribute should be used as the final object key, so there are only objects, no arrays. This attribute always has to be unique in order to work correctly.
Is it possible to combine or extend the existing nest function to solve the above points?
The pros of this method is, that instead of the keys, I can also use an array of functions (p => p.parameterGroup1) to return the parameter. So instead of a last optional parameter, I could also use p => p.parameterGroup1 ? p.parameterGroup1 : p.label
I prepared a little test, to give you a better idea of what is expected:
test('nest array of objects by groups as keys, stopping at null and using a final label param', t => {
let properties = [
{
parameterGroup1: 'first',
parameterGroup2: 'second',
parameterGroup3: 'third',
label: 'A'
},
{
parameterGroup1: 'first',
parameterGroup2: 'second',
parameterGroup3: null,
label: 'B'
},
{
parameterGroup1: 'a',
parameterGroup2: 'b',
parameterGroup3: undefined,
label: 'C'
},
]
let expected = {
first: {
second: {
third: {
A: {
parameterGroup1: 'first',
parameterGroup2: 'second',
parameterGroup3: 'third',
label: 'A'
}
},
B: {
parameterGroup1: 'first',
parameterGroup2: 'second',
parameterGroup3: null,
label: 'B'
}
}
},
a: {
b: {
C: {
parameterGroup1: 'a',
parameterGroup2: 'b',
parameterGroup3: undefined,
label: 'C'
}
}
}
}
let grouped = nest(properties, ['parameterGroup1', 'parameterGroup2', 'parameterGroup3'], 'label')
t.deepEqual(grouped, expected)
})
Thank you in advance!
Here is a way to do it in vanilla js. We construct the result object by reduceing the array seq: For each object obj in the array seq, we walk the result object level by level using the values from obj of the keys from keys. If the value is null or undefined, we skip (won't go down another level). If the value exist we go down a level, creating a level (object) if it doen't already exist. We do that repeatedly using a reduce on the keys array untill we find the leaf object (last level), to which we assign our current object under the key obtained evaluating obj[last]:
function nest(seq, keys, last) {
return seq.reduce((result, obj) => {
// First we find the (last level) object to which we will assign our current object to, as a child
let lastLevel = keys.reduce((res, key) => { // for each key in keys
let value = obj[key]; // get the value from our current object obj for that key key
if(value == null) return res; // if the value is null or undefined, skip
if(res[value]) return res[value]; // if the level for value exists return it
return res[value] = {}; // if it doesn't, create a new level, assing it to result and return it
}, result);
// then we assign it using the value of the key last
lastLevel[obj[last]] = obj; // we found the last possible level, assign obj to it under the key obj[last]
return result;
}, {});
}
Example:
function nest(seq, keys, last) {
return seq.reduce((result, obj) => {
let lastLevel = keys.reduce((res, key) => {
let value = obj[key];
if(!value) return res;
if(res[value]) return res[value];
return res[value] = {};
}, result);
lastLevel[obj[last]] = obj;
return result;
}, {});
}
let properties = [{parameterGroup1: 'first',parameterGroup2: 'second',parameterGroup3: 'third',label: 'A'},{parameterGroup1: 'first',parameterGroup2: 'second',parameterGroup3: null,label: 'B'},{parameterGroup1: 'a',parameterGroup2: 'b',parameterGroup3: undefined,label: 'C'}];
let grouped = nest(properties, ['parameterGroup1', 'parameterGroup2', 'parameterGroup3'], 'label');
console.log(grouped);

Categories

Resources