Related
I got a schema object that looks like this:
const schema = {
social: {
facebook: 'someValue',
twitter: {
department: {
departmentImage: {
editable: 'someValue'
}
}
}
}
};
The editable property indicates a value that I want to edit, and may appear in several nested locations in the object.
My approach to edit it is to recursively create a new object who is an exact copy of the original, and populate a new value where I encounter editable.
Like this:
const formatSchema = (schema, data, formattedSchema = {}) => {
for (const schemaKey in schema) {
const firstKey = Object.keys(schema[schemaKey])[0];
if (schema[schemaKey] instanceof Object) {
formattedSchema[schemaKey] = schema[schemaKey];
formatschema(schema[schemaKey], data, formattedSchema[schemaKey]);
}
if (schema[schemaKey] instanceof Object && firstKey === 'editable') {
*replacing data logic*
formattedSchema[schemaKey] = ...*replacingData*;
formatschema(schema[schemaKey], data, formattedSchema[schemaKey]);
} else {
formattedSchema[schemaKey] = schema[schemaKey];
}
}
return formattedSchema;
};
But I feel this solution may be inefficient as I create every single bit of the object from scratch and this would happen thousands of times a day.
Is there a way to do it better?
Here's a recursive immutable update that works for any native input type. Don't worry about performance here as it's plenty fast, even if your object has thousands of fields. Let me know how this suits you and I can make a change if it's needed -
function update(t, func) {
switch (t?.constructor) {
case Object:
return Object.fromEntries(
Object.entries(t).map(([k,v]) =>
[k, func([k, update(v, func)])]
)
)
case Array:
return t.map((v, k) => func([k, update(v, func)]))
default:
return func([null, t])
}
}
const schema = {
social: {
facebook: 'someValue',
twitter: {
department: {
departmentImage: {
editable: 'someValue'
}
},
someArr: [{ editable: 1 }, { editable: 2 }, { hello: "world" }]
},
}
}
console.log(update(schema, ([k,v]) =>
k == "editable" ? "✅" : v
))
.as-console-wrapper {min-height: 100% !important; top: 0}
{
"social": {
"facebook": "someValue",
"twitter": {
"department": {
"departmentImage": {
"editable": "✅"
}
},
"someArr": [
{
"editable": "✅"
},
{
"editable": "✅"
},
{
"hello": "world"
}
]
}
}
}
I am trying to get the change object from two objects using typescript in angular.
For example
this.productPreviousCommand = {
"id": "60f910d7d03dbd2ca3b3dfd5",
"active": true,
"title": "ss",
"description": "<p>ss</p>",
"category": {
"id": "60cec05df64bde4ab9cf7460"
},
"subCategory": {
"id": "60cec18c56d3d958c4791117"
},
"vendor": {
"id": "60ced45b56d3d958c479111c"
},
"type": "load_product_success"
}
model = {
"active": true,
"title": "ss",
"description": "<p>ss sss</p>",
"category": "60cec05df64bde4ab9cf7460",
"subCategory": "60cec18c56d3d958c4791117",
"vendor": "60ced45b56d3d958c479111c",
"tags": []
}
Now the difference between two objects are description: "<p>hello hello 1</p>". So I want to return {description: "<p>hello hello 1</p>"}
I used lodash https://github.com/lodash/lodash
import { transform, isEqual, isObject, isArray} from 'lodash';
function difference(origObj, newObj) {
function changes(newObj, origObj) {
let arrayIndexCounter = 0
return transform(newObj, function (result, value, key) {
if (!isEqual(value, origObj[key])) {
let resultKey = isArray(origObj) ? arrayIndexCounter++ : key
result[resultKey] = (isObject(value) && isObject(origObj[key])) ? changes(value, origObj[key]) : value
}
})
}
return changes(newObj, origObj)
}
This library is not working for me, it returns the whole object using this code const differenc = difference(this.productPreviousCommand, model);
The output of above code is
{
active: true
description: "<p>hello hello 1</p>"
id: "60f8f29dd03dbd2ca3b3dfd1"
title: "hello"
}
Try this function
differenceInObj(firstObj: any, secondObj: any): any {
let differenceObj: any = {};
for (const key in firstObj) {
if (Object.prototype.hasOwnProperty.call(firstObj, key)) {
if(firstObj[key] !== secondObj[key]) {
differenceObj[key] = firstObj[key];
}
}
}
return differenceObj;
}
You can check loop through each key of the first object and compare it with the second object.
function getPropertyDifferences(obj1, obj2) {
return Object.entries(obj1).reduce((diff, [key, value]) => {
// Check if the property exists in obj2.
if (obj2.hasOwnProperty(key)) {
const val = obj2[key];
// Check if obj1's property's value is different from obj2's.
if (val !== value) {
return {
...diff,
[key]: val,
};
}
}
// Otherwise, just return the previous diff object.
return diff;
}, {});
}
const a = {
active: true,
description: '<p>hello</p>',
id: '60f8f29dd03dbd2ca3b3dfd1',
title: 'hello',
};
const b = {
active: true,
description: '<p>hello hello 1</p>',
id: '60f8f29dd03dbd2ca3b3dfd1',
title: 'hello',
};
const c = {
active: true,
description: '<p>hello hello 2</p>',
id: '60f8f29dd03dbd2ca3b3dfd1',
title: 'world',
};
console.log(getPropertyDifferences(a, b));
console.log(getPropertyDifferences(b, c));
function difference(origObj, newObj) {
const origObjKeyList = Object.keys(origObj),
newObjKeyList = Object.keys(newObj);
// if objects length is not same
if (origObjKeyList?.length !== newObjKeyList?.length) {
return;
}
// if object keys some difference in keys
if (Object.keys(origObj).filter((val) => !Object.keys(newObj).includes(val))?.length) {
return;
}
return Object.entries(origObj).reduce(
(acc, [key, value]) => (newObj[key] !== value ? { ...acc, ...{ [key]: newObj[key] } } : acc),
[]
);
}
const a = {
active: true,
description: '<p>hello</p>',
id: '60f8f29dd03dbd2ca3b3dfd1',
title: 'hello',
};
const b = {
active: true,
description: '<p>hello hello 1</p>',
id: '60f8f29dd03dbd2ca3b3dfd1',
title: 'hello',
};
console.log(difference(a, b));
You can try this code.
function difference(origObj, newObj) {
const origObjKeyList = Object.keys(origObj),
newObjKeyList = Object.keys(newObj);
// if objects length is not same
if (origObjKeyList?.length !== newObjKeyList?.length) {
return;
}
// if object keys is not same
if (Object.keys(origObj).filter((val) => !Object.keys(newObj).includes(val))?.length) {
return;
}
return Object.entries(origObj).reduce(
(acc, [key, value]) => (newObj[key] !== value ? { ...acc, ...{ [key]: newObj[key] } } : acc),
[]
);
}
I have object as below
const myObject = {
"Obj1" :[ {
name:"test",
down:"No"
Up: "Yes",
},
{ }, {}....
],
"Obj2" :[ {}, {}......
],
"Obj3" : [ {}, {}, {....
],
}
I want to clone above object and want to modified "Obj1" if name="test" then make it as Up to "Yes"
Basically I want to conditionally spread object property.
Well the question is a bit unclear. Anyway,
if you want to conditionally update 'up' if 'name' === 'test' from a cloned object:
import { cloneDeep } from lodash/fp;
// you can use also JSON stringify + JSON parse
// spread operator will only shallow copy your object
//clone it
const deepClonedObject = cloneDeep(myObject);
// update items accordingly to your needs
deepClonedObject.obj1 = deepClonedObject.obj1.map(item => (item.name === 'test'
? { ...item, up: 'Yes' }
: item)
)
You can do it combine reduce and map. reduce because it versatile and can return object instead array and map to update properties.
const myObject = {
Obj1: [
{
name:"test",
down:"No",
Up: "No",
},
],
Obj2: [
{
name:"test",
down:"No",
Up: "No",
},
],
Obj3: [
{
name:"test2",
down:"No",
Up: "No",
},
],
};
const updatedObject = Object.keys(myObject).reduce((obj, key) => {
return {
...obj,
[key]: myObject[key].map((newObject) => {
if (newObject.name === 'test') {
newObject.Up = 'Yes';
}
return newObject;
})
};
}, {});
console.log(updatedObject);
We have input like this
const params = {
id: 1,
filters: {
price: {
min: 101,
max: 300
}
},
sorters: {
sortBy: 'price',
order: 'desc'
}
}
and we want output to look like this id=1&min=101&max=300&sortBy=price&order=desc.
Object is nested. If we have object with just 1 level (no object within objects) then we could simply do
for (const [key, value] of Object.entries(object1)) {
arr.push(`${key}=${value}`);
}
then probably .join array with &. But here in this case this is not enough. My first though was to just inner this for loops like this:
const paramsStringify = (paramsObject) => {
const arr = []
for (const [key, value] of Object.entries(paramsObject)) {
if(typeof value === 'object') {
for (const [key2, value2] of Object.entries(value)) {
if(typeof value2 === 'object') {
for (const [key3, value3] of Object.entries(value2)) {
console.log('3rd lvl', `${key3}: ${value3}`);
arr.push(`${key3}=${value3}`)
}
} else {
console.log('2nd lvl', `${key2}: ${value2}`);
arr.push(`${key2}=${value2}`)
}
}
} else {
console.log('1st lvl', `${key}: ${value}`);
arr.push(`${key}=${value}`)
}
}
console.log('arr', arr.join('&'))
}
this is working fine but code look awful to me. I nested it three times, if I would have even more nested objects I would need to put another for loop there. But the pattern is same for each level of objects. Can I somehow refactor it so it would work even if I have input with 7 nested objects?
You could use a URLSearchParams object and recursively append key value pairs to it. This will automatically encode values.
const params = {
id: 1,
filters: { price: { min: 101, max: 300 } },
sorters: { sortBy: "price", order: "desc" }
}
function getParam(o, searchParam = new URLSearchParams) {
Object.entries(o).forEach(([k, v]) => {
if (v !== null && typeof v === 'object')
getParam(v, searchParam)
else
searchParam.append(k, v)
})
return searchParam
}
const searchParam = getParam(params);
console.log(
searchParam.toString()
)
const params = {
id: 1,
filters: {
price: {
min: 101,
max: 300
}
},
sorters: {
sortBy: 'price',
order: 'desc'
}
}
function add(a, o) {
for (const [key, value] of Object.entries(o)) {
if (typeof value === 'object') add(a, value); // recursion
else a.push(`${key}=${value}`); // only if value not-an-object
}
return a;
}
console.log(add([], params).join("&"))
This is a simple recursive version. You will probably have to tune it if you want to support more complex structures (arrays, duplicate keys, ...)
Assuming your object adheres to your example, use a URLSearchParams object and avoid recursion using a regex
const params = {
id: 1,
filters: {
price: {
min: 101,
max: 300
}
},
sorters: {
sortBy: 'price',
order: 'desc'
}
}
const js = JSON.stringify(params);
const re = /"(\w+)":[^\{]?"?(\w+)"?/g
const usp = new URLSearchParams();
js.match(re).forEach(str => {
const [key,val] = str.split(":");
usp.set(key.replace(/"/g,""),val.replace(/"/g,""))
})
console.log(usp.toString())
You need to create a recursive function if you don't want to program the same logic for each nested object. See https://www.javascripttutorial.net/javascript-recursive-function/ for more information about recursive functions.
For a nested complex object or array, I would like to collect all values for a given property name. Example:
var structure = {
name: 'alpha',
array: [
{ name: 'beta' },
{ name: 'gamma' }
],
object: {
name: 'delta',
array: [
{ name: 'epsilon' }
]
}
};
// expected result: [ 'alpha', 'beta', 'gamma', 'delta', 'epsilon' ]
It's obvious how to achieve this using plain JS, but: Is there any elegant, concise approach using lodash?
[edit] Current variant below. Nicer solutions welcome!
function getPropertyRecursive(obj, property) {
var values = [];
_.each(obj, function(value, key) {
if (key === property) {
values.push(value);
} else if (_.isObject(value)) {
values = values.concat(getPropertyRecursive(value, property));
}
});
return values;
}
This can be done elegantly with the following mixin, which is a recursive version of _.toPairs:
_.mixin({
toPairsDeep: obj => _.flatMap(
_.toPairs(obj), ([k, v]) =>
_.isObjectLike(v) ? _.toPairsDeep(v) : [[k, v]])
});
then to get the result you want:
result = _(structure)
.toPairsDeep()
.map(1)
.value()
If there are scalar properties other than name, you'll have to filter them out:
result = _(structure)
.toPairsDeep()
.filter(([k, v]) => k === 'name')
.map(1)
.value()
There's no Lodash/Underscore function that I know if that will do what you're looking for.
So what are you looking to do? Well, specifically you're looking to extract the values of all of the name properties out of a aggregate structure. How would we generalize that? In other words, if you were looking to add such functionality to Lodash/Underscore, how would you reframe the problem? After all, most people don't want to get the values of the name properties. You could create a generic function where you supply the name of the property you want, but...thinking even more abstractly than that, what you really want to do is visit all of the nodes in a aggregate structure and do something with them. If we consider aggregate structures in JavaScript as generic trees, we can take a recursive approach using a depth-first walk:
function walk(o, f) {
f(o);
if(typeof o !== 'object') return;
if(Array.isArray(o))
return o.forEach(e => walk(e, f));
for(let prop in o) walk(o[prop], f);
}
Now we can do what you're looking for by walking the structure and adding things to an array:
const arr = [];
walk(structure, x => if(x !== undefined && x.name) arr.push(x.name));
This isn't quite functional enough for my tastes, though...there's a side effect on arr here. So an even better generic approach (IMO) would be to allow a context object to ride along (or an accumulator if you will, a la Array#reduce):
function walk(o, f, context) {
f(o, context);
if(typeof o !== 'object') return context;
if(Array.isArray(o)) return o.forEach(e => walk(e, f, context)), context;
for(let prop in o) walk(o[prop], f, context);
return context;
}
Now you can call it like this, side-effect free:
const arr = walk(structure, (x, context) => {
if(x !== undefined && x.name) context.push(x.name);
}, []);
Iterate the object recursively using _.reduce():
function getPropertyRecursive(obj, prop) {
return _.reduce(obj, function(result, value, key) {
if (key === prop) {
result.push(value);
} else if (_.isObjectLike(value)) {
return result.concat(getPropertyRecursive(value, prop));
}
return result;
}, []);
}
var structure = {
name: 'alpha',
array: [{
name: 'beta'
}, {
name: 'gamma'
}],
object: {
name: 'delta',
array: [{
name: 'epsilon'
}]
}
};
var result = getPropertyRecursive(structure, 'name');
console.log(result);
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.16.2/lodash.min.js"></script>
You could iterate the object and call it again for arrays or objects. Then get the wanted property.
'use strict';
function getProperty(object, key) {
function iter(a) {
var item = this ? this[a] : a;
if (this && a === key) {
return result.push(item);
}
if (Array.isArray(item)) {
return item.forEach(iter);
}
if (item !== null && typeof item === 'object') {
return Object.keys(item).forEach(iter, item);
}
}
var result = [];
Object.keys(object).forEach(iter, object);
return result;
}
var structure = { name: 'alpha', array: [{ name: 'beta' }, { name: 'gamma' }], object: { name: 'delta', array: [{ name: 'epsilon' }] } };
console.log(getProperty(structure,'name'));
.as-console-wrapper { max-height: 100% !important; top: 0; }
Based on the answer ( https://stackoverflow.com/a/39822193/3443096 ) , here's another idea for mixin:
_.mixin({
extractLeaves: (obj, filter, subnode, subpathKey, rootPath, pathSeparator) => {
var filterKv = _(filter).toPairs().flatMap().value()
var arr = _.isArray(obj) ? obj : [obj]
return _.flatMap(arr, (v, k) => {
if (v[filterKv[0]] === filterKv[1]) {
var vClone = _.clone(v)
delete vClone[subnode]
vClone._absolutePath = rootPath + pathSeparator + vClone[subpathKey]
return vClone
} else {
var newRootPath = rootPath
if (_.isArray(obj)) {
newRootPath = rootPath + pathSeparator + v[subpathKey]
}
return _.extractLeaves(
v[subnode], filter, subnode,
subpathKey, newRootPath, pathSeparator
)
}
})
}
});
This work for this example JSON, where you want to extract leaf-nodes:
{
"name": "raka",
"type": "dir",
"children": [{
"name": "riki",
"type": "dir",
"children": [{
"name": "roko",
"type": "file"
}]
}]
}
Use it this way:
_.extractLeaves(result, {type: "file"}, "children", "name", "/myHome/raka", "/")
And you will get:
[
{
"name": "roko",
"type": "file",
"_absolutePath": "/myHome/raka/riki/roko"
}
]