Merging Objects Using Vanilla JavaScript - javascript

I've been working on an event handler to sync inputs with the state and came up with this solution:
https://gist.github.com/jadeallencook/678869f988bd65378d819496c4343e78
The handler pushes the value to the end point using the string and merges the result with the current state but I have to use lodash to accomplish it, is there anyway to do it in vanilla?

A reduce will produce a generic solution:
const path = 'foo-bar-num';
const paths = path.split('-');
const object = {};
const value = 'Woo hoo!';
paths.reduce(([object, value], path, idx) => {
object[path] = idx === paths.length - 1 ? value : {};
return [object[path], value];
}, [object, value]);
console.log(object)

Alternatively you can try lodash set function to set value for deeply nested object.
import set from 'lodash/set'
let path = 'foo-num';
const paths = path.split('-').join('.')
const value = 'some value';
set(object, paths, value)

You could store the last key and reduce only the keys before. Ath the end store the previous value and assign the new value.
function setValue(object, keys, value) {
var last = keys.pop(),
temp = keys.reduce((o, k) => o[k] = o[k] || {}, object);
temp.original = !(last in temp) ? null : temp[last];
temp[last] = value;
}
const
object = {},
path = 'foo-bar-num',
value = 'Woo hoo!';
setValue(object, path.split('-'), value);
console.log(object);

Related

Assign object values conditionally JavaScript

I am receiving a data object from an api where some of its properties may have a value of null. I want to create a function that returns the object but for the properties that have a null value there is a "-" instead. I have tried:
const hyphenOrContent = movie => {
return Object.values(movie).map(val => {
return val === null ? "-" : val
});
}
but this only returns an array of the values. I've read that it's better not to use a for in loop so I would like to avoid using one if possible. Any help appreciated!
Map the object's entries, turning nulls into -s, then use Object.fromEntries to turn the array of entries into an object:
const hyphenOrContent = movie => Object.fromEntries(
Object.entries(movie).map(
([key, val]) => ([key, val === null ? "-" : val])
)
);
Just loop over the Object and alter the properties.
const hyphenOrContent = movie => {
Object.entries(movie).forEach(([key, val]) => {
if (val === null) movie[key] = '-';
});
};
var a = {
foo: null,
bar: '123',
baz: null
}
hyphenOrContent(a);
console.log(a);
If you do not want to alter the original, you can clone it.

How to build a menu list object recursively in JavaScript?

With an array of
['/social/swipes/women', '/social/swipes/men', '/upgrade/premium'];
I'd like to construct an map object that looks like:
{
'social': {
swipes: {
women: null,
men: null
}
},
'upgrade': {
premium: null
}
}
const menu = ['/social/swipes/women', '/social/likes/men', '/upgrade/premium'];
const map = {};
const addLabelToMap = (root, label) => {
if(!map[root]) map[root] = {};
if(!map[root][label]) map[root][label] = {};
}
const buildMenuMap = menu => {
menu
// make a copy of menu
// .slice returns a copy of the original array
.slice()
// convert the string to an array by splitting the /'s
// remove the first one as it's empty
// .map returns a new array
.map(item => item.split('/').splice(1))
// iterate through each array and its elements
.forEach((element) => {
let root = map[element[0]] || "";
for (let i = 1; i < element.length; i++) {
const label = element[i];
addLabelToMap(root, label)
// set root to [root][label]
//root = ?
root = root[label];
}
});
}
buildMenuMap(menu);
console.log(map);
But I'm unsure how to switch the value of root.
What do I set root to so that it recursively calls addLabelToMap with
'[social]', 'swipes' => '[social][swipes]', 'women' => '[social][swipes]', 'men'?
I've used root = root[element] but it's giving an error.
Alternative solutions would be great, but I'd like to understand why this isn't working fundamentally.
This problem is about creating the object and maintaining it's state while looping through input array and splitting string based upon /.
This can be accomplished using Array.reduce where we start with empty object and while looping through input we start filling it and for last word in every string we assign the value null to object property.
let input = ['/social/swipes/women', '/social/swipes/men', '/upgrade/premium'];
let output = input.reduce((o, d) => {
let keys = d.split('/').filter(d => d)
keys.reduce((k, v, i) => {
k[v] = (i != keys.length - 1)
? k[v] || {}
: null
return k[v]
}, o)
return o
}, {})
console.log(output)
Use reduce instead of map. The root will be the accumulator in this case:
const buildMenuMap = menu =>
menu.reduce((root, item) => {
let parts = item.slice(1).split("/");
let lastPart = parts.pop();
let leaf = parts.reduce((acc, part) => acc[part] || (acc[part] = {}), root);
leaf[lastPart] = null;
return root;
}, Object.create(null));
Explanation:
For each item in the menu array, we extract the parts by first getting rid of the leading '/' (using slice(1)) and then splitting by '/'.
We then remove the lastPart from this resulting array (the last part is handled separetely from the rest).
For each remaining part in the parts array, we traverse the root array. At each level of traversing, we either return the object at that level acc[part] if it already exists, or we create and return a new one if it doesn't (acc[part] = {}).
After we get to the the last level leaf, we use the lastPart to set the value as null.
Notice that we pass Object.create(null) to reduce. Object.create(null) creates a prototypeless object so it will ba safer to use root[someKey] without having to check if someKey is an owned property or not.
Example:
const buildMenuMap = menu =>
menu.reduce((root, item) => {
let parts = item.slice(1).split("/");
let lastPart = parts.pop();
let leaf = parts.reduce((acc, part) => acc[part] || (acc[part] = {}), root);
leaf[lastPart] = null;
return root;
}, Object.create(null));
let arr = ['/social/swipes/women', '/social/swipes/men', '/upgrade/premium'];
let result = buildMenuMap(arr);
console.log(result);
It is as easy as:
root = root[label];
if you change your helper function to:
const addLabelToMap = (root, label) => {
if(!root[label]) root[label] = {};
}
I'd write it as:
const buildMenuMap = menus => {
const root = {};
for(const menu of menus) {
const keys = menu.split("/").slice(1);
const prop = keys.pop();
const obj = keys.reduce((curr, key) => curr[key] || (curr[key] = {}), root);
obj[prop] = null;
}
return root;
}
I just debugged your code to see what was wrong and I urge you to do the same. You make two (obvious) mistakes:
Firstly, In the very first iteration, here the value of map is just an empty object {}, the value of root gets initialised to "" and label is swipes.
.forEach((element) => {
let root = map[element[0]] || "";
...
root = root[label];
}
So then you get root[label] is undefined and so the new root is undefined.
Second, you are using map everywhere as it is.
const addLabelToMap = (root, label) => {
if(!map[root]) map[root] = {};
if(!map[root][label]) map[root][label] = {};
}
Instead you should be taking it as a parameter, for you to be able to do a recursion.
const addLabelToMap = (root, label) => {
if(!root[label]) root[label] = {};
}
To debug you code, create a simple HTML file with the js in the script tags and then serve it from your local machine using python -m http.server. You can then add a debug point and go through your code step by step.
Try this as a holistic solution:
const menu = ['/social/swipes/women', '/social/swipes/men', '/upgrade/premium'];
const deepMerge = (target, source) => {
// Iterate through `source` properties and if an `Object` set property to merge of `target` and `source` properties
for (let key of Object.keys(source)) {
if (source[key] instanceof Object && key in target) Object.assign(source[key], deepMerge(target[key], source[key]))
}
// Join `target` and modified `source`
Object.assign(target || {}, source)
return target
};
const buildMenuMap = menu => {
return menu
.map(item => item.split('/').splice(1))
// The `root` value is the object that we will be merging all directories into
.reduce((root, directory) => {
// Iterates backwards through each directory array, stacking the previous accumulated object into the current one
const branch = directory.slice().reverse().reduce((acc, cur) => { const obj = {}; obj[cur] = acc; return obj;},null);
// Uses the `deepMerge()` method to stitch together the accumulated `root` object with the newly constructed `branch` object.
return deepMerge(root, branch);
}, {});
};
buildMenuMap(menu);
Note: The deep merge solution was taken from #ahtcx on GitHubGist
You can simplify your code using Array.reduce, Object.keys & String.substring
buildMenuMap
The function takes the array as input and reduce it into an object where for each entry in array, the object is updated with corresponding hierarchy using addLabelToMap function. Each entry is converted into an array of levels (c.substring(1).split("/")).
addLabelToMap
The function takes 2 inputs
obj - the current root object / node
ar - array of child hierarchy
and returns the updated object
Logic
function pops the first value (let key = ar.shift()) as key and add / update in the object (obj[key] = obj[key] || {};).
If there is child hierarchy of current object (if(ar.length)), recursively call the function to update the object till the end (addLabelToMap(obj[key], ar)).
Else (no further child hierarchy), check whether the object has some hierarchy (else if(!Object.keys(obj[key]).length)) because of other entries in array. If there is no hierarchy, i.e. it is a leaf, hence, set the value to null (obj[key] = null). Note, if there will never be case where there is an entry in array like /social/swipes/men/young along with existing, the else if block can be simplified to a simple else block.
The object has been update, return the final updated object
let arr = ['/social/swipes/women', '/social/swipes/men', '/upgrade/premium'];
function addLabelToMap(obj, ar) {
let key = ar.shift();
obj[key] = obj[key] || {};
if(ar.length) addLabelToMap(obj[key], ar);
else if(!Object.keys(obj[key]).length) obj[key] = null;
return obj;
}
function buildMenuMap(ar) {
return ar.reduce((a,c) => addLabelToMap(a,c.substring(1).split("/")), {});
}
console.log(buildMenuMap(arr));
You can solve this also with a recursive function in a concise way like this:
let obj={}, input = ['/social/swipes/women', '/social/swipes/men', '/upgrade/premium'];
const makeObj = (arr, obj={}) => {
let prop = arr.shift()
prop in obj ? null : Object.assign(obj, {[prop]: {}})
arr.length ? makeObj(arr, obj[prop]) : obj[prop] = null
return obj
}
input.forEach(x => makeObj(x.split('/').filter(Boolean), obj))
console.log(obj)
The idea is for each of the paths to pass them to a makeObj function which will decorate an object with the paths recursively until it reaches the end of the path array. This is another alternative to the common Array.reduce approach.
Citate from bounty description:
The current answers do not contain enough detail.
I think you do not understand how it works in current answers. Because of this I will provide you two solutions: one alternative solution and one expanded solution with Array.reduce() function.
Alternative solution with for loops
The explanation of code see in the code comments.
var arr = ['/social/swipes/women', '/social/swipes/men', '/upgrade/premium'],
result = {};
//if you want to have it shorter you can write for(var i = arr.length; i--;) too
for(var i = 0; i < arr.length; i++)
{
var parts = arr[i].slice(1).split('/'),
//if we have already one object then we take it. If not the we create new one:
curObj = result[parts[0]] = result[parts[0]] || {};
for(var k = 1; k < parts.length; k++)
{
//if we have next part
if(parts[k+1])
//if we have already one object then we take it. If not the we create new one:
curObj[parts[k]] = curObj[parts[k]] || {};
//if we do not have next part
else curObj[parts[k]] = null;
//or if-else block in one line:
//curObj[parts[k]] = parts[k+1] ? (curObj[parts[k]] || {}) : null;
//link to next object:
curObj = curObj[parts[k]];
}
}
console.log(JSON.stringify(result, null, 4));
But if you did not understand it then see this code:
var arr = ['/social/swipes/women', '/social/swipes/men', '/upgrade/premium'],
result = {},
parts = arr[2].slice(1).split('/'),
curObj = result[parts[0]] = {};
curObj[parts[1]] = parts[1+1] ? {} : null;
console.log(JSON.stringify(result, null, 4));
Expanded solution with Array.reduce()
In this solution I use the code from user Nitish Narang in expanded version with some explanation in comments and console output – so yuo can see in console what the code does. My recommendation: if you do not understand the code with arrow functions then write it full with normal functions and appropriate variable names which explain themselves. We (humans) need some pictures to imagine all things. If we have only some short variable names then it is difficalt to imagine und undestand all this. I have also a little bit «shorted» his code.
var arr = ['/social/swipes/women', '/social/swipes/men', '/upgrade/premium'];
var result = arr.reduce(function(acc0, curVal, curIdx)
{
console.log('\n' + curIdx + ': ------------\n'
+ JSON.stringify(acc0, null, 4));
var keys = curVal.slice(1).split('/');
keys.reduce(function(acc1, currentValue, currentIndex)
{
acc1[currentValue] = keys[currentIndex+1]
? acc1[currentValue] || {}
: null;
return acc1[currentValue]
}, acc0); //acc0 - initialValue is the same object, but it is empty only in first cycle
return acc0
}, {}); // {} - initialValue is empty object
console.log('\n------result------\n'
+ JSON.stringify(result, null, 4));

JS rename an object key, while preserving its position in the object

My javascript object looks like this:
const someObj = {
arr1: ["str1", "str2"],
arr2: ["str3", "str4"]
}
In attempting to rename a key (e.g. arr1), I end up deleting the existing key and writing a new key with the original value. The order of obj changes.
someObj = {
arr2: ["str3", "str4"],
renamedarr1: ["str1", "str2"]
}
How do I rename a key while preserving the key order?
In the end it was solved in a js-vanila way rather than a react way.
In case somebody would look for a similar solution, I am posting the code I ended up using. Inspired by Luke's idea:
const renameObjKey = ({oldObj, oldKey, newKey}) => {
const keys = Object.keys(oldObj);
const newObj = keys.reduce((acc, val)=>{
if(val === oldKey){
acc[newKey] = oldObj[oldKey];
}
else {
acc[val] = oldObj[val];
}
return acc;
}, {});
return newObj;
};
You might want to consider reducing the array of keys into a new object.
To do this, you need also to know which key changed to what.
Reduce the array of keys
use a reducer which checks for a key change, and change it if necessary.
add the key to the object with the value
After that you have a Object with the order you had before, and possibly a changed key is still at the same position
Something like this might work (not tested)
const changedKeyMap = {"previousKey": "newKey"};
const keys = Object.keys(this.state.obj);
const content = e.target.value;
const result = keys.reduce((acc, val) => {
// modify key, if necessary
if (!!changedKeyMap[val]) {
val = changedKeyMap[val];
}
acc[val] = content;
// or acc[val] = this.state.obj[val] ?
return acc;
}, {});
As you can see, you need to keep track of how you changed a key (changedKeyMap).
The reduce function just iterates over all keys in correct order and adds them to a newly created object. if a key is changed, you can check it in the changedKeyMap and replace it. It will still be at the correct position

Converting an array of objects in to a single object

Have an array which contains a no of json .
[{linkValue:"value1"},{linkValue:"value2"},{linkValue:"value3"},{linkValue:"value4"},{linkValue:"value5"}]
Note that each Json have same key . I want to convert this array into a single json like
{linkValue1:"value1",linkValue2:"value2",linkValue3:"value3",linkValue4:"value4",linkValue5:"value5"}
one thing i also need to know . my array is also inside a json how i get this arry from that json ?
My initial json is
{name:"value",age:"value",linkValue:[[{linkValue:"value1"},{linkValue:"value2"},{linkValue:"value3"},{linkValue:"value4"},{linkValue:"value5"}]
]}
I'm expcting my final json look like :
{name:"value",age:"value",linkValue1:"value1",linkValue2:"value2",linkValue3:"value3",linkValue4:"value4",linkValue5:"value5"}
can anyone please help me
Use Array.forEach and add properties to an empty object:
let source = {name:"value",age:"value",linkValue:[[{linkValue:"value1"},{linkValue:"value2"},{linkValue:"value3"},{linkValue:"value4"},{linkValue:"value5"}]]};
// Copy the array in a variable
let yourArray = source.linkValue[0];
// Delete the original array in the source object
delete source.linkValue;
yourArray.forEach((item, index) => {
source["linkValue" + (index + 1)] = item.linkValue
});
console.log(source); // Should have what you want
Using reduce API,
let targetObj = srcArr.reduce((accumulator, value, index)=>{
accumulator['linkValue'+(index+1)] = value.linkValue;
return accumulator;
}, {});
[{linkValue:"value1"},{linkValue:"value2"},{linkValue:"value3"},{linkValue:"value4"},{linkValue:"value5"}]
This is javascript Array contains multiple javascript object.
{linkValue1:"value1",linkValue2:"value2",linkValue3:"value3",linkValue4:"value4",linkValue5:"value5"}
If you need structure like this,Then define a single javascript object and add linkvalue1,linkvalue2 etc. as a member of that object and then add it to javascript Array.
Give this a try.
myObj.linkValue = myObj.linkValue.map((obj, index) => ({ [`linkValue${index + 1}`]: obj.linkValue }))
Solution with reduce
// HELPER FUNCTIONS
// pick properties from object with default value
function pick (props, sourceObj, defaultValue) {
return props.reduce(
(obj, prop) =>
Object.assign(obj, { [prop]: sourceObj[prop] || defaultValue }),
{}
)
}
// get property value or default
function propOr (propName, obj, defaultValue) {
return obj[propName] || defaultValue
}
// flatten nested array
function flattern (nestedArray) {
return nestedArray.reduce((flat, innerArray) => flat.concat(innerArray), [])
}
// LINKS BUILDER based on REDUCE
function buildLinks (linksArray) {
return linksArray.reduce((accumulator, item, index) => {
accumulator['linkValue' + (index + 1)] = item.linkValue
return accumulator
}, {})
}
// TRANSFORMATION FUNCTION - takes json and produce required output
function transform(json) {
return Object.assign(
{},
pick(['name', 'age'], json, null),
buildLinks(flattern(propOr('linkValue', json, [])))
)
}
// EXECUTION
const result = transform(source)
PS> You can use libraries like Lodash or Ramda and replace helper functions defined by me with ones from library

How to replace/name keys in a Javascript key:value object?

How should I replace the key strings in a Javascript key:value hash map (as an object)?
This is what I have so far:
var hashmap = {"aaa":"foo", "bbb":"bar"};
console.log("before:");
console.log(hashmap);
Object.keys(hashmap).forEach(function(key){
key = key + "xxx";
console.log("changing:");
console.log(key);
});
console.log("after:");
console.log(hashmap);
See it running in this jsbin.
The "before" and "after" hashmaps are the same, so the forEach seems to be in a different scope. How can I fix it? Perhaps there are better ways of doing this?
It has nothing to do with scope. key is just a local variable, it's not an alias for the actual object key, so assigning it doesn't change the object.
Object.keys(hashmap).forEach(function(key) {
var newkey = key + "xxx";
hashmap[newkey] = hashmap[key];
delete hashmap[key];
});
if keys order is important you can use:
const clone = Object.fromEntries(
Object.entries(o).map(([o_key, o_val]) => {
if (o_key === key) return [newKey, o_val];
return [o_key, o_val];
})
);
this will create an object with the new key in the same place where the old one was.
You are just changing the copy of the object's keys, so the original object won't be changed. You can create an new object to hold the new keys, like this:
var hashmap = {"aaa":"foo", "bbb":"bar"};
console.log("before:");
console.log(hashmap);
var newHashmap = {};
Object.keys(hashmap).forEach(function(key){
var value = hashmap[key];
key = key + "xxx";
console.log("changing:");
console.log(key);
newHashmap[key] = value;
});
console.log("after:");
console.log(newHashmap);
You can use Array.prototype.reduce().
const hashmap = { aaa: 'foo', bbb: 'bar' };
const newHashmap = Object.entries(hashmap).reduce((acc, [key, value]) => ({
...acc,
[`${key}xxx`]: value,
}), {});
console.log(newHashmap);
// { aaaxxx: 'foo', bbbxxx: 'bar' }
The function takes as an argument the original map and returns a new one with the keys altered. The call of the mapT(m) just returns the transformed map with the new keys.
function mapT(map){
const x = new Map();
for (const [k, v] of map) {
x.set(k+ "xxx", v);
}
return x;
}
Simple call : var new_map = mapT(mapA);

Categories

Resources