I have a function that accepts the following arguments:
set(section, field, pair, component, element, value)
The section, field, pair and component are just keys within the Object. They are way-points so we can travel down the hierarchy. Obviously section is the head, our entry point.
element is the target key and the value is the value that will be set.
Since, there are elements at different depths, I would like to do the following:
set('utility', null, null, null, 'exportId', 'banana')
This is for a shallow access, and internally it will do this:
dataObj[section][element] = value;
**/ As in
* data: {
utility: {
exportId: 'banana'
}
* }
*/
In other cases, when the element is deeper inside the Object, it may be required to do the following:
dataObj[section][field][pair][component][element] = value;
What would be the best way, to define the path to the element dynamically, so we skip the keys that are passed in as a 'null'?
for example:
set('history', 'current', null, null, 'fruit', 'apple')
**/ As in
* data: {
history: {
current: {
fruit: 'apple'
}
}
* }
*/
will internally be constructed as:
dataObj[section][field][element] = value;
as you might have noticed, we skipped [pair][component] because those slots were passed in as null(s).
Instead of having a long list of specific parameters, just pass an object to the function. this way you only pass what you need to and there won't be any "null" references to deal with on the call.
Using this implementation, this call can be shortened to something like this:
Your current implementation:
set('utility', 'current', null, null, 'exportId', 'banana')
Using an object as the parameter:
set({
section:'utility',
field:'current',
element: 'exportId',
value:'banana'
});
You could use rest parameters to get the arguments passed to an array. Then create an object using reduceRight
function set(...paths) {
return paths.reduceRight((r, key, i) => key !== null ? { [key] : r } : r)
}
console.log(set('history', 'current', null, null, 'fruit', 'apple'))
console.log(set('utility', null, null, null, 'exportId', 'banana'))
The above function will construct a nested object based on the paths. If you want to just update an existing object, you could traverse the object and set the value like this:
function set(dataObj, ...paths) {
let value = paths.pop(),
nested = dataObj;
for (let i = 0; i < paths.length; i++) {
const path = paths[i];
if (i === paths.length - 1 && nested)
nested[path] = value; // set the value if it's the final item
if (path !== null && nested)
nested = nested[path]; // get another level of nesting
}
return dataObj;
}
let obj = { utility: { exportId: 'banana' } }
console.log(set(obj, 'utility', null, null, null, 'exportId', 'orange'))
obj = {
history: {
current: {
fruit: 'apple'
}
}
}
console.log(set(obj, 'history', 'current', null, null, 'fruit', 'orange'))
Related
I have the following variable on my state:
this.state = {
playerList: {
player: [
{
playerAlias: [
{
name: null
}
],
idPlayer: null,
playerName: null,
broadcastChannel: null,
clusterName: null
}
]
}
}
what I wanted to do is to delete a certain item on that same list,and for that I wrote this method:
delete = (player) => {
let listAux = this.state.playerList.player;
let newList = [];
listAux.map((playerAux) => {
if (playerAux != player) {
newList.push(playerAux)
}
})
this.setState({
playerList:newList
})
}
I though that this would work but it does not, the following error appear when trying to iterate all my elements of my variable on the render method:
Cannot read map of undefined, when trying to execute this for cycle this.state.playerList.player.map((player)
A few things:
Array#map is designed to create a new array with your modified values. Array#forEach is the appropriate method to use in this situation. But in this case you shouldn't be using either, because:
You're essentially reinventing Array#filter. Think of filter as doing the same operation with the exact opposite logic: Only keep values in my existing array if playerAux != player
const newList = this.state.playerList.player.filter((playerAux) => {
return playerAux != player
})
this.setState({ playerList: newList })
EDIT: Keep in mind what playerAux is. It's actually the object itself:
{
playerAlias: [
{
name: null
}
],
idPlayer: null,
playerName: null,
broadcastChannel: null,
clusterName: null
}
I not 100% sure what you're trying to compare, but it's probably playerName, which would make it playerAux.playerName != player
Array of objects where the hierarchy objects is stored in hierarchy
property. nesting of objects are done based on this hierarchy
[
{
"hierarchy" : ["obj1"],
"prop1":"value"
},
{
"hierarchy" : ["obj1","obj2"],
"prop2":"value",
"prop3":"value"
},
{
"hierarchy" : ["obj1","obj3"],
"prop4":"value",
"prop5":"value"
},
{
"hierarchy" : ["obj1","obj3", "obj4"],
"prop6":"value",
"prop7":"value",
"arr" :["val1", "val2"]
}
]
Expected nested object, hierarchy key removed here
{
"obj1":{
"prop1":"value",
"obj2" : {
"prop2":"value",
"prop3":"value"
},
"obj3":{
"prop4":"value",
"prop5":"value",
"obj4" : {
"prop6":"value",
"prop7":"value",
"arr" :["val1", "val2"]
}
}
}
}
Code I tried but at line 8 unable to get the hierarchy
var input = "nested array as above";
var output = {};
var globalTemp = output;
for(var i = 0 ; i<input.length ; i++){
var tempObj = input[i];
for(var key in tempObj){
if(key == "hierarchy"){
globalTemp = globlalTemp[tempObj[key]] = {};
}
}
}
console.log(globalTemp);
You can use forEach and reduce methods and inside create shallow copy of current object and delete hierarchy property.
const data = [{"hierarchy":["obj1"],"prop1":"value"},{"hierarchy":["obj1","obj2"],"prop2":"value","prop3":"value"},{"hierarchy":["obj1","obj3"],"prop4":"value","prop5":"value"},{"hierarchy":["obj1","obj3","obj4"],"prop6":"value","prop7":"value","arr":["val1","val2"]}]
const result = {}
data.forEach(function(o) {
o.hierarchy.reduce(function(r, e) {
const clone = Object.assign({}, o);
delete clone.hierarchy
return r[e] = (r[e] || clone)
}, result)
})
console.log(result)
With a newer version of javascript, you could use restparameters for the wanted value key/value pairs and build a nested structure by iterating the given hierarchy property by saving the last property for assigning the rest properties.
The reclaimed part getFlat uses an array as stack without recursive calls to prevent a depth first search which tries to get the most depth nodes first.
At start, the stack is an array with an array of the actual object and another object with an empty hierarchy property with an empty array, because actually no key of the object is known.
Then a while loop checks if the stack has some items and if so, it takes the first item of the stack and takes a destructuring assignment for getting an object o for getting back all key/value pairs and another object temp with a single property hierarchy with an array of the path to the object o.
The push flag is set to false, because only found properties should be pushed later to the result set.
Now all properties of the object are checked and if
the value is truthy (to prevent null values),
the type is an object (null is an object) and
the property is not an array
then a new object is found to inspect. This object is pushed to the stack with the actual path to it.
If not, then a value is found. This key/value pair is added to the temp object and the flag is set to true, for later pushing to the result set.
Proceed with the keys of the object.
Later check push and push temp object with hierarchy property and custom properties to the result set.
function getFlat(object) {
var stack = [[object, { hierarchy: [] }]],
result = [],
temp, o, push;
while (stack.length) {
[o, temp] = stack.shift();
push = false;
Object.keys(o).forEach(k => {
if (o[k] && typeof o[k] === 'object' && !Array.isArray(o[k])) {
stack.push([o[k], { hierarchy: temp.hierarchy.concat(k) }]);
} else {
temp[k] = o[k];
push = true;
}
});
push && result.push(temp);
}
return result;
}
var data = [{ hierarchy: ["obj1"], prop1: "value" }, { hierarchy: ["obj1", "obj2"], prop2: "value", prop3: "value" }, { hierarchy: ["obj1", "obj3"], prop4: "value", prop5: "value" }, { hierarchy: ["obj1", "obj3", "obj4"], prop6: "value", prop7: "value", arr: ["val1", "val2"] }],
object = data.reduce((r, { hierarchy, ...rest }) => {
var last = hierarchy.pop();
hierarchy.reduce((o, k) => o[k] = o[k] || {}, r)[last] = rest;
return r;
}, {}),
reclaimedData = getFlat(object);
console.log(object);
console.log(reclaimedData);
.as-console-wrapper { max-height: 100% !important; top: 0; }
I have an object, which has multiple children (this object is a serialized MongoDB record)
{
_id: '5881f6564d56a24f09562d9e',
key: 'value',
child: {
_id: '5882211a010ea9725a3efdd1',
key: 'value2',
param: 'param',
nested: {
_id: '588221592eb1530d6fcc252a',
arr: [ '588221b83f0f833ba132b670', '588224490a15d836d1ba56e4' ]
}
},
another: {
_id: '58822c4e48db7912655b3419',
param: 'value'
}
}
Before using this object in my application, I need to pass it through a function.
function processData(value) {
// do stuff
return value
}
However, this function (not controlled by me) doesn't support nested documents. To correctly process the object, it must start with the deepest nested document, replace it with the return value, then process the next level etc.
A 'document', is an object which has the key _id. There may be other objects without _id, these do not need to be processed. Therefore, it needs to be processed in the following order:
obj.child.nested = processData(obj.child.nested)
obj.child = processData(obj.child)
obj.another = processData(obj.another)
obj = processData(obj)
The order only matters for objects which have nested children (for example, obj.another could be processed before obj.child, as long as obj.child.nested was processed before obj.child).
This is what I have so far: http://jsbin.com/nenuvuwiwa/edit?js,console
This is what I ended up using:
function processData(obj) {
// Placeholder function to indicate it has
// been processed (in reality sets a load of
// prototype functions etc)
obj.processed = true;
return obj;
}
function processDoc(doc) {
for (key in doc) {
var val = doc[key];
if (val.hasOwnProperty('_id')) {
val = processDoc(val);
val = processData(val)
}
}
return doc;
}
var res = processDoc(obj)
As per title, I need to clone an object in Javascript like that below and set each values to zero. Of course the object properties can change.
{ _id: { action: null, date: null },
avg: null,
min: null,
max: null,
total: null }
// helper method to get the correct object type
function toType(x) {
return ({}).toString.call(x).match(/\s([a-zA-Z]+)/)[1].toLowerCase();
}
// recursive function that sets all properties to null
// except objects which it passes back into the reset function
function reset(obj) {
// clone the object
var out = JSON.parse(JSON.stringify(obj));
for (var p in out) {
if (toType(out[p]) === 'object') {
reset(out[p]);
} else {
out[p] = null;
}
}
return out;
}
reset(obj);
DEMO
I have an array of objects that can be of any length and any depth. I need to be able to find an object by its id and then modify that object within the array. Is there an efficient way to do this with either lodash or pure js?
I thought I could create an array of indexes that led to the object but constructing the expression to access the object with these indexes seems overly complex / unnecessary
edit1; thanks for all yours replies I will try and be more specific. i am currently finding the location of the object I am trying to modify like so. parents is an array of ids for each parent the target object has. ancestors might be a better name for this array. costCenters is the array of objects that contains the object I want to modify. this function recurses and returns an array of indexes that lead to the object I want to modify
var findAncestorsIdxs = function(parents, costCenters, startingIdx, parentsIdxs) {
var idx = startingIdx ? startingIdx : 0;
var pidx = parentsIdxs ? parentsIdxs : [];
_.each(costCenters, function(cc, ccIdx) {
if(cc.id === parents[idx]) {
console.log(pidx);
idx = idx + 1;
pidx.push(ccIdx);
console.log(pidx);
pidx = findAncestorsIdx(parents, costCenters[ccIdx].children, idx, pidx);
}
});
return pidx;
};
Now with this array of indexes how do I target and modify the exact object I want? I have tried this where ancestors is the array of indexes, costCenters is the array with the object to be modified and parent is the new value to be assigned to the target object
var setParentThroughAncestors = function(ancestors, costCenters, parent) {
var ccs = costCenters;
var depth = ancestors.length;
var ancestor = costCenters[ancestors[0]];
for(i = 1; i < depth; i++) {
ancestor = ancestor.children[ancestors[i]];
}
ancestor = parent;
console.log(ccs);
return ccs;
};
this is obviously just returning the unmodified costCenters array so the only other way I can see to target that object is to construct the expression like myObjects[idx1].children[2].grandchildren[3].ggranchildren[4].something = newValue. is that the only way? if so what is the best way to do that?
You can use JSON.stringify for this. It provides a callback for each visited key/value pair (at any depth), with the ability to skip or replace.
The function below returns a function which searches for objects with the specified ID and invokes the specified transform callback on them:
function scan(id, transform) {
return function(obj) {
return JSON.parse(JSON.stringify(obj, function(key, value) {
if (typeof value === 'object' && value !== null && value.id === id) {
return transform(value);
} else {
return value;
}
}));
}
If as the problem is stated, you have an array of objects, and a parallel array of ids in each object whose containing objects are to be modified, and an array of transformation functions, then it's just a matter of wrapping the above as
for (i = 0; i < objects.length; i++) {
scan(ids[i], transforms[i])(objects[i]);
}
Due to restrictions on JSON.stringify, this approach will fail if there are circular references in the object, and omit functions, regexps, and symbol-keyed properties if you care.
See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_native_JSON#The_replacer_parameter for more info.
As Felix Kling said, you can iterate recursively over all objects.
// Overly-complex array
var myArray = {
keyOne: {},
keyTwo: {
myId: {a: '3'}
}
};
var searchId = 'myId', // Your search key
foundValue, // Populated with the searched object
found = false; // Internal flag for iterate()
// Recursive function searching through array
function iterate(haystack) {
if (typeof haystack !== 'object' || haystack === null) return; // type-safety
if (typeof haystack[searchId] !== 'undefined') {
found = true;
foundValue = haystack[searchId];
return;
} else {
for (var i in haystack) {
// avoid circular reference infinite loop & skip inherited properties
if (haystack===haystack[i] || !haystack.hasOwnProperty(i)) continue;
iterate(haystack[i]);
if (found === true) return;
}
}
}
// USAGE / RESULT
iterate(myArray);
console.log(foundValue); // {a: '3'}
foundValue.b = 4; // Updating foundValue also updates myArray
console.log(myArray.keyTwo.myId); // {a: '3', b: 4}
All JS object assignations are passed as reference in JS. See this for a complete tutorial on objects :)
Edit: Thanks #torazaburo for suggestions for a better code.
If each object has property with the same name that stores other nested objects, you can use: https://github.com/dominik791/obj-traverse
findAndModifyFirst() method should solve your problem. The first parameter is a root object, not array, so you should create it at first:
var rootObj = {
name: 'rootObject',
children: [
{
'name': 'child1',
children: [ ... ]
},
{
'name': 'child2',
children: [ ... ]
}
]
};
Then use findAndModifyFirst() method:
findAndModifyFirst(rootObj, 'children', { id: 1 }, replacementObject)
replacementObject is whatever object that should replace the object that has id equal to 1.
You can try it using demo app:
https://dominik791.github.io/obj-traverse-demo/
Here's an example that extensively uses lodash. It enables you to transform a deeply nested value based on its key or its value.
const _ = require("lodash")
const flattenKeys = (obj, path = []) => (!_.isObject(obj) ? { [path.join('.')]: obj } : _.reduce(obj, (cum, next, key) => _.merge(cum, flattenKeys(next, [...path, key])), {}));
const registrations = [{
key: "123",
responses:
{
category: 'first',
},
}]
function jsonTransform (json, conditionFn, modifyFn) {
// transform { responses: { category: 'first' } } to { 'responses.category': 'first' }
const flattenedKeys = Object.keys(flattenKeys(json));
// Easily iterate over the flat json
for(let i = 0; i < flattenedKeys.length; i++) {
const key = flattenedKeys[i];
const value = _.get(json, key)
// Did the condition match the one we passed?
if(conditionFn(key, value)) {
// Replace the value to the new one
_.set(json, key, modifyFn(key, value))
}
}
return json
}
// Let's transform all 'first' values to 'FIRST'
const modifiedCategory = jsonTransform(registrations, (key, value) => value === "first", (key, value) => value = value.toUpperCase())
console.log('modifiedCategory --', modifiedCategory)
// Outputs: modifiedCategory -- [ { key: '123', responses: { category: 'FIRST' } } ]
I needed to modify deeply nested objects too, and found no acceptable tool for that purpose. Then I've made this and pushed it to npm.
https://www.npmjs.com/package/find-and
This small [TypeScript-friendly] lib can help with modifying nested objects in a lodash manner. E.g.,
var findAnd = require("find-and");
const data = {
name: 'One',
description: 'Description',
children: [
{
id: 1,
name: 'Two',
},
{
id: 2,
name: 'Three',
},
],
};
findAnd.changeProps(data, { id: 2 }, { name: 'Foo' });
outputs
{
name: 'One',
description: 'Description',
children: [
{
id: 1,
name: 'Two',
},
{
id: 2,
name: 'Foo',
},
],
}
https://runkit.com/embed/bn2hpyfex60e
Hope this could help someone else.
I wrote this code recently to do exactly this, as my backend is rails and wants keys like:
first_name
and my front end is react, so keys are like:
firstName
And these keys are almost always deeply nested:
user: {
firstName: "Bob",
lastName: "Smith",
email: "bob#email.com"
}
Becomes:
user: {
first_name: "Bob",
last_name: "Smith",
email: "bob#email.com"
}
Here is the code
function snakeCase(camelCase) {
return camelCase.replace(/([A-Z])/g, "_$1").toLowerCase()
}
export function snakeCasedObj(obj) {
return Object.keys(obj).reduce(
(acc, key) => ({
...acc,
[snakeCase(key)]: typeof obj[key] === "object" ? snakeCasedObj(obj[key]) : obj[key],
}), {},
);
}
Feel free to change the transform to whatever makes sense for you!