_.assign only if property exists in target object - javascript

My need is to do something like an _.assign, but only if the target object already has the property being assigned. Think of it like the source objects may have some properties to contribute, but also some properties that I don't want to mix in.
I haven't ever used _.assign's callback mechanism, but tried the following. It 'worked', but it still assigned the property to the dest object (as undefined). I don't want it to assign at all.
_.assign(options, defaults, initial, function (destVal, sourceVal) {
return typeof destVal == 'undefined' ? undefined : sourceVal;
});
I wrote the following function to do this, but wondering if lodash already has something baked in that is more elegant.
function softMerge (dest, source) {
return Object.keys(dest).reduce(function (dest, key) {
var sourceVal = source[key];
if (!_.isUndefined(sourceVal)) {
dest[key] = sourceVal;
}
return dest;
}, dest);
}

You could take just the keys from the first object
var firstKeys = _.keys(options);
Then take a subset object from the second object, taking only those keys which exist on the first object :
var newDefaults = _.pick(defaults, firstKeys);
Then use that new object as your argument to _.assign :
_.assign(options, newDefaults);
Or in one line :
_.assign(options, _.pick(defaults, _.keys(options)));
Seemed to work when I tested it here : http://jsbin.com/yiyerosabi/1/edit?js,console

Here is a immutable deep version, I call it "merge that retains the shape", in TypeScript that uses lodash:
function _mergeKeepShapeArray(dest: Array<any>, source: Array<any>) {
if (source.length != dest.length) {
return dest;
}
let ret = [];
dest.forEach((v, i) => {
ret[i] = _mergeKeepShape(v, source[i]);
});
return ret;
}
function _mergeKeepShapeObject(dest: Object, source: Object) {
let ret = {};
Object.keys(dest).forEach((key) => {
let sourceValue = source[key];
if (typeof sourceValue !== "undefined") {
ret[key] = _mergeKeepShape(dest[key], sourceValue);
} else {
ret[key] = dest[key];
}
});
return ret;
}
function _mergeKeepShape(dest, source) {
// else if order matters here, because _.isObject is true for arrays also
if (_.isArray(dest)) {
if (!_.isArray(source)) {
return dest;
}
return _mergeKeepShapeArray(dest, source);
} else if (_.isObject(dest)) {
if (!_.isObject(source)) {
return dest;
}
return _mergeKeepShapeObject(dest, source);
} else {
return source;
}
}
/**
* Immutable merge that retains the shape of the `existingValue`
*/
export const mergeKeepShape = <T>(existingValue: T, extendingValue): T => {
return _mergeKeepShape(existingValue, extendingValue);
}
And a simple test to see how I vision such merge should work:
let newObject = mergeKeepShape(
{
a : 5,
// b is not here
c : 33,
d : {
e : 5,
// f is not here
g : [1,1,1],
h : [2,2,2],
i : [4,4,4],
}
},
{
a : 123,
b : 444,
// c is not here
d : {
e : 321,
f : 432,
// g is not here
h : [3,3,3],
i : [1,2],
}
}
);
expect(newObject).toEqual({
a : 123,
// b is not here
c : 33,
d : {
e : 321,
// f is not here,
g : [1,1,1],
h : [3,3,3],
i : [4,4,4]
}
});
I used seamless-immutable myself in the test, but didn't see a need to put it in this answer.
I hereby place this in the Public Domain.

Another way to accomplish this is by combining _.mapObject with _.has
_.mapObject(object1, function(v, k) {
return _.has(object2, k) ? object2[k] : v;
});
Explanation:
Traverse all key/value pairs of object1 using _.mapObject
Using _.has, check if property name k also exists in object2.
If it does, copy the value assigned to key object2's k back to object1, else, just return the existing value of object1 (v).
Plunkr

Following #svarog's answer I came up with this (lodash version 4.17.15):
const mergeExistingProps = (target, source) => _.mapValues(target, (value, prop) => _.get(source, prop, value));

I recently have the same need in my personal project, I need to fill the value from one object(SOURCE) to another object(TARGET) but don't expand its property. Also, some additional requirements should be met:
Any property with a null value in the source will not update to the target;
Any value from the source can be updated into target if such property in target has null value.
The property that holds an array in the target will be loaded based on data from the source, but all entries of the array will remain the same as the target array (so an empty array in the target will not get any data since the item has no property)
Property of the target holding a 2-d array (array has another array as its item) will not be updated, since the meaning of merging two 2-d arrays with a different shape is not clear to me.
Below is an example (Detailed explained in the code):
Assume you have a resume object holding all the data about you, you want to fill the data into the company's application form (also an object). You want the result to have the identical shape of the application form since the company doesn't care about other things, then you can think your resume is SOURCE and the application form is TARGET.
Note that the "additional" field in TARGET is null, which means anything can be updated here based on SOURCE data (As rule #2)
The console output is in JSON format, copy it to some JSON to JS-OBJ converter such as
https://www.convertsimple.com/convert-json-to-javascript/
to have a better view
const applicationForm = {
name: 'Your Name',
gender: 'Your Gender',
email: 'your#email.com',
birth: 0,
experience: [ // employer want you list all your experience
{
company: 'Some Company',
salary: 0,
city: ['', '', ''], // list all city worked for each company
}
],
language: { // employer only care about 2 language skills
english: {
read: false,
write: false,
speak: 'Speak Level'
},
chinese: {
read: false,
write: false,
speak: 'Speak Level'
}
},
additional: null // add anything you want the employer to know
}
const resume = {
name: 'Yunfan',
gender: 'Male',
birth: 1995,
phone: '1234567',
email: 'example#gmail.com',
experience: [
{
company: 'Company A',
salary: 100,
city: ['New York', 'Chicago', 'Beijing'],
id: '0001',
department: 'R&D'
},
{
company: 'Company B',
salary: 200,
city: ['New York'],
id: '0002',
department: 'HR'
},
{
company: 'Company C',
salary: 300,
city: ['Tokyo'],
id: '0003',
}
],
language: {
english: {
read: true,
write: true,
speak: 'Native Speaker'
},
chinese: {
read: true,
write: false,
speak: 'HSK Level 3'
},
spanish: {
read: true,
write: true,
speak: 'Native Speaker'
}
},
additional: {
music: 'Piano',
hometown: 'China',
interest: ['Cooking', 'Swimming']
}
}
function safeMerge(source, target) {
// traverse the keys in the source object, if key not found in target or with different type, drop it, otherwise:
// 1. Use object merge if the value is an object (Can go deeper inside the object and apply same rule on all its properties)
// 2. Use array merge if value is array (Extend the array item from source, but keep the obj format of target)
// 3. Assign the value in other case (For other type, no need go deeper, assign directly)
for (const key in source) {
let value = source[key]
const targetValueType = typeof target[key]
const sourceValueType = typeof value
// if key not found in target or type not match
if (targetValueType === 'undefined' || targetValueType !== sourceValueType) {
continue // property not found in target or type not match
}
// for both type in object, need additional check
else if (targetValueType === 'object' && sourceValueType === 'object') {
// if value in target is null, assign any value from source to target, ignore format
if (target[key] === null) {
target[key] = source[key]
}
// if value in target is array, merge the item in source to target using the format of target only if source value is array
else if (Array.isArray(target[key]) && Array.isArray(value)) {
target[key] = mergeArray(value, target[key])
}
// if value in target is 'real' object (not null or array)', use object merge to do recurring merge, keep target format
else if (!Array.isArray(target[key])){
if (!Array.isArray(value) && value !== null) {
safeMerge(value, target[key])
}
}
}
// if target value and source value has same type but not object, assign directly
else if (targetValueType === sourceValueType) {
target[key] = value
}
}
}
function mergeArray(sourceArray, targetArray) {
// the rule of array merge need additional declare, assume the target already have values or objects in save format in the property<Array>,
// otherwise will not merge item from source to target since cannot add item property,
// NOTE: the item in target array will be totally overwrite instead of append on the tail, only the format will be keep,
// so the lenth of this property will same as source, below is a example:
// target = [{a: 1, b: 2}, {a: 3, b: 4}] // Must in same format, otherwise the first one will be standard
// source = [{a: 5, b: 6, c: 7}]
// mergeArray(source, target) => [{a: 5, b: 6}] // use format of target, but data from source
// double check both of values are array
if (!Array.isArray(sourceArray) || !Array.isArray(targetArray)) {
return
}
// if target array is empty, don't push data in, since format is empty
if (targetArray.length === 0) {
return
}
let resultArray = [] // array to save the result
let targetFormat = targetArray[0]
let targetArrayType = typeof targetArray[0]
// assign value from source to target, if item in target array is not object
if (targetArrayType !== 'object'){
sourceArray.forEach((value) => {
// assign value directly if the type matched
if (targetArrayType === typeof value) {
resultArray.push(value)
}
})
}
// if the item in target is null, push anything in source to target (accept any format)
else if (targetArray[0] === null) {
sourceArray.forEach((value) => {
resultArray.push(value)
})
}
// if the item in target is array, drop it (the meaning of merge 2-d array to a 2-d array is not clear, so skip the situation)
else if (!Array.isArray(targetArray[0])){
// the item is a 'real' object, do object merge based on format of first item of target array
sourceArray.forEach((value) => {
safeMerge(value, targetFormat) // data in targetFormat keep changing, so need to save a independent copy to the result
resultArray.push(JSON.parse(JSON.stringify(targetFormat)))
})
}
else {
console.log('2-d array will be skipped')
}
// replace the value of target with newly built array (Assign result to target array will not work, must assign outside)
return resultArray
}
safeMerge(resume, applicationForm)
console.log(JSON.stringify(applicationForm))

Related

How to check if an object has other than specific properties

I have an object obj which has n number of possible properties
lets say some of them are known,
const someKnownProps = ["props.abc", "xyz"]; // or more
I want to know if obj has other than known properties in it.
To clarify:
obj can look like this:
obj = {
props: {
abc: {
def: 1
},
ghi: {
jkl: 2
}
},
xyz: 3
}
Doing Object.keys only return first level children,
in this case it will return props not props.abc
You can use Object.keys to get all keys and filter the keys which aren't included in the someKnownProps array.
const obj = {
"props.abc": 1,
"xyz": 2,
"three": 3,
"four": 4,
}
const someKnownProps = ["props.abc", "xyz"]; // or more
const unknownKeys = Object.keys(obj).filter(key => !someKnownProps.includes(key))
console.log(unknownKeys)
There are two (unrelated) tasks involved in this question:
Traversal of an object's properties
Comparison of a set of traversed object properties to a list of strings representing dot-notation-formatted object property accessors
While I'm sure the former has been previously discussed on SO, I'll provide an implementation of such an algorithm below in order to address the details of this question.
This is essentially a specific case of recursion where each cycle starts with these inputs:
an object
a dot-notation-formatted path
a Set of existing such paths
The code below includes inline comments explaining what's happening, and there are some console.log statements at the end to help you visualize some example results based on the data in your question. If something is unclear after reviewing the code, feel free to leave a comment.
'use strict';
/** #returns whether value is a non-null, non-array object */
function isObject (value) {
return value !== null && typeof value === 'object' && !Array.isArray(value);
}
/** #returns the enumerable (optionally including inherited) keys of an object */
function getKeys (obj, includeInherited = false) {
if (!includeInherited) return Object.keys(obj);
const keys = new Set();
let o = obj;
while (o !== null) {
for (const key of Object.keys(o)) keys.add(key);
o = Object.getPrototypeOf(o);
}
return [...keys];
}
/**
* #returns an array of strings representing all traversible branches
* of child objects, each formatted as a combined path of dot-notation
* property accessors
*/
function findObjectPaths (
obj,
{
includeInherited = false,
currentPath = '',
paths = new Set(),
skipReturn = false,
} = {},
) {
for (const key of getKeys(obj, includeInherited)) {
// Append the current dot-notation property accessor
// to the existing path of this object:
const path = `${currentPath}.${key}`;
// Add it to the set:
paths.add(path);
const o = obj[key];
// Recurse if the child value is an object:
if (isObject(o)) {
findObjectPaths(o, {
includeInherited,
currentPath: path,
paths,
skipReturn: true,
});
}
}
// If this is not a sub-cycle (it's the top-level invocation), then convert
// the set to an array and remove the first "." from each string
if (!skipReturn) return [...paths].map(p => p.slice(1));
}
// Use:
const obj = {
props: {
abc: {
def: 1,
},
ghi: {
jkl: 2,
},
},
xyz: 3,
};
let someKnownProps = ['props.abc', 'xyz'];
let objectPaths = findObjectPaths(obj);
let hasOtherProps = objectPaths.some(path => !someKnownProps.includes(path));
console.log(hasOtherProps); // true
// An example of all of the paths in the object above:
someKnownProps = [
'props',
'props.abc',
'props.abc.def',
'props.ghi',
'props.ghi.jkl',
'xyz',
];
objectPaths = findObjectPaths(obj);
hasOtherProps = objectPaths.some(path => !someKnownProps.includes(path));
console.log(hasOtherProps); // false
// Finally, comparing the results of inherited vs non-inherited enumeration:
const objWithoutOwnProps = Object.create({
props: {
abc: {
def: 1,
},
ghi: {
jkl: 2,
},
},
xyz: 3,
});
console.log(
'Non-inherited props:',
findObjectPaths(objWithoutOwnProps),
);
console.log(
'Inherited props:',
findObjectPaths(objWithoutOwnProps, {includeInherited: true}),
);
Similar to what Mina said:
let obj = {one: 1, two: 2, three: 3};
let knownKeys = ['one', 'two'];
for (let key in obj) {
if (!knownKeys.includes(key)) {
console.log(key);
}
}

What's the most efficient way to compare values of two objects?

Let's say I have two objects, which might have some properties in common. The overall idea is that one is used as a schema to compare it to the other. I would like to compare values of common properties and construct a new object representing these comparisons of each individual property. In the result, any properties in common should have the value of comparing the source properties for equality (e.g. result.a.b.c = (obj1.a.b.c == obj2.a.b.c)). Any properties that exist on only one of the two objects would be copied to the result with their original values.
For example, consider the following objects to compare:
const schema = {
num: 123,
str: 'hello',
nested: {
num: 123,
str: 'hello',
},
},
doc = {
nums: [1,2,3],
str: 'hello',
nested: {
num: 123,
str: 'world',
foo: 'bar',
},
};
The comparison result should be:
{
nums: [1,2,3], // obj2.nums
num: 123, // obj1.num
str: true, // obj1.str == obj2.str
nested: {
num: true, // obj1.nested.num == obj2.nested.num
str: false, // obj1.nested.str == obj2.nested.str
foo: 'bar', // obj2.nested.foo
},
}
How would I perform this comparison in the most efficient time and space way?
Currently I have this implementation:
function deepCompare(obj1, obj2) {
const validatedObject = {}
Object.keys(obj1).forEach(e => {
if(object[e].constructor.name === 'Object') {
validatedObject[e] = deepCompare(obj1[e], obj2[e])
} else {
validatedObject[e] = obj1[e] === obj2[e]
}
})
return validatedObject
}
Are there any ways to do this more efficiently without using JSON.stringify, because I might have property that is missing, but I still want to return the fact that the values are correct despite the schema of those objects?
Maybe for in or for of loop? But how would I test their effectiveness? It's okay for small objects, but sometimes they get really big and I want to compare them in the most efficient way possible.
NOTE for editors.
I understand that this might seem like a duplicate, but it in fact is not, here I'm looking for a most efficient way to compare values, iteratively, recursively or some other way, not objects themselves
the logic is if obj1 has a key and obj2 has the same key, we compare the values of those keys
Then you should use
function compare(a, b) {
if (a === b) return true;
if (typeof a == 'object' && a != null && typeof b == 'object' && b != null) {
return Object.keys(a).every(k =>
!(k in b) || compare(a[k], b[k])
);
}
return false;
}
I think your solution is good enough, it's only missing some default checks (you don't need to iterate through the objects keys if you have the same refference for example).
Also, there's this package that claims is the fastest deep equal checker.
You can look over the code to see if you missed any other condition (it's still a recursive solution for the objects, but there are some other data types treated there if you're interested).
There is also this article if you prefer written material instead of code that has more checks before recursive calls
What you want to do is first find the common object keys. Then as you loop through the properties you want to return false as soon as you find a mismatch. In the snippet below I'm using an every loop which will stop as soon as one iteration returns false. If you return a value in the if block you do not need the else block.
function deepCompare(obj1, obj2) {
const commonKeys = Object.keys(obj1).filter(x => Object.keys(obj2).includes(x));
let valid = true;
commonKeys.every(e => {
if (obj1[e].constructor.name === 'Object') {
console.log('object', e, obj1[e], obj2[e])
valid = deepCompare(obj1[e], obj2[e])
return valid // true goes to next, false ends the .every loop
}
console.log('not object', e, obj1[e] === obj2[e] ? 'match' : 'mismatch')
valid = obj1[e] === obj2[e]
return valid // true goes to next, false ends the .every loop
})
return valid // returns the first false or true if everything passes
}
const obj1 = {
num: 123,
str: 'hello',
nested: {
num: 123,
str: 'hello',
}
}
const obj3 = {
num: 1233,
str: 'hello'
}
const obj4 = {
num: 123,
str: 'hello',
nested: {
num: 123,
str: 'hellure',
}
}
console.log(deepCompare(obj1, obj1.nested))
console.log(deepCompare(obj1, obj3))
console.log(deepCompare(obj1, obj4))

i wanna return correctly children's object. how can i?

function Ha8(arr, id) {
let result = [];
for(let i = 0; i < arr.length; i++) {
if(Array.isArray(arr[i].children)) {
// if it is a array, it going to be run recursive
result.push(arr[i].children)
const col = Ha8(result[i], id);
if(col === id) {
// find it in array in array
return result
// then return the id object,
} else {
continue; // still can't find.. go ahead!
}
} else if (arr[i]['id']===id) {
return arr[i] // will return valid id object
}
return null // if its none , return null, or parameter id is undefined.
}
}
I m write Intended direction. but its not work..
how can i fix ? give me some tip please.
let input = [
{
id: 1,
name: 'johnny',
},
{
id: 2,
name: 'ingi',
children: [
{
id: 3,
name: 'johnson',
},
{
id: 5,
name: 'steve',
children: [
{
id: 6,
name: 'lisa',
},
],
},
{
id: 11,
},
],
},
{
id: '13',
},
];
output = Ha8(input, 5);
console.log(output); // --> { id: 5, name: 'steve', children: [{ id: 6, name: 'lisa' }] }
output = Ha8(input, 99);
console.log(output); // --> null
I wanna return like that, but only return 'null' ..
need to check children's id and return children's object by using recursive.
so i write like that. but i have no idea..
how to return correctly children id's element?
I will give you an answer using a totally different approach, and using the magic of the JSON.stringify() method, more specifically the replacer optional parameter, which allows the use of a callback function that can be used as a filter.
As you can see, it simplifies a lot the final code. It could also be modified to introduce not only an id, but also any key or value, as I did in my final approach.
EDIT: Following your suggestion, as you prefer your function to be recursive, I recommend you to use the Array.reduce() method. It allows an elegant iteration through all the properties until the needs are met.
Using null as initial value, which is the last argument of the reduce method, it allows to iterate through all fields in the array in the following way:
The first if will always be skipped on the first iteration, as the initial value is null.
The second if will set the currentValue to the accumulator if the property id exists and is equal to the value you are trying to find
The third if, which you could add an Array.isArray() to add a type validation, will check if the property children exists. As it is the last one, it will only work if all the other conditions aren't met. If this property exists, it will call again Ha8Recursive in order to start again the process.
Finally, if neither of this works, it should return null. The absence of this last condition would return undefined if the input id doesn't exist
const Ha8 = (array, inputKey, inputValue) => {
let children = null;
JSON.stringify(array, (key, value) => {
if (value[inputKey] && value[inputKey] === inputValue) {
children = value;
}
return value;
});
return children;
};
const Ha8Recursive = (array, inputKey, inputValue) => {
return array.reduce((accumulator, currentValue) => {
if (accumulator) {
return accumulator;
} else if (currentValue[inputKey] && currentValue[inputKey] === inputValue) {
return currentValue;
} else if (currentValue.children) {
return Ha8Recursive(currentValue.children, inputKey, inputValue);
} else {
return null;
}
}, null)
}
const input = [{"id":1,"name":"johnny"},{"id":2,"name":"ingi","children":[{"id":3,"name":"johnson"},{"id":5,"name":"steve","children":[{"id":6,"name":"lisa"}]},{"id":11}]},{"id":"13"}];
console.log('JSON stringify function');
console.log(Ha8(input, 'id', 5));
console.log('Recursive function')
console.log(Ha8Recursive(input, 'id', 5));

JS proxy SET function does not get triggered when object property value gets changed [duplicate]

I have this code in js bin:
var validator = {
set (target, key, value) {
console.log(target);
console.log(key);
console.log(value);
if(isObject(target[key])){
}
return true
}
}
var person = {
firstName: "alfred",
lastName: "john",
inner: {
salary: 8250,
Proffesion: ".NET Developer"
}
}
var proxy = new Proxy(person, validator)
proxy.inner.salary = 'foo'
if i do proxy.inner.salary = 555; it does not work.
However if i do proxy.firstName = "Anne", then it works great.
I do not understand why it does not work Recursively.
http://jsbin.com/dinerotiwe/edit?html,js,console
You can add a get trap and return a new proxy with validator as a handler:
var validator = {
get(target, key) {
if (typeof target[key] === 'object' && target[key] !== null) {
return new Proxy(target[key], validator)
} else {
return target[key];
}
},
set (target, key, value) {
console.log(target);
console.log(key);
console.log(value);
return true
}
}
var person = {
firstName: "alfred",
lastName: "john",
inner: {
salary: 8250,
Proffesion: ".NET Developer"
}
}
var proxy = new Proxy(person, validator)
proxy.inner.salary = 'foo'
A slight modification on the example by Michał Perłakowski with the benefit of this approach being that the nested proxy is only created once rather than every time a value is accessed.
If the property of the proxy being accessed is an object or array, the value of the property is replaced with another proxy. The isProxy property in the getter is used to detect whether the currently accessed object is a proxy or not. You may want to change the name of isProxy to avoid naming collisions with properties of stored objects.
Note: the nested proxy is defined in the getter rather than the setter so it is only created if the data is actually used somewhere. This may or may not suit your use-case.
const handler = {
get(target, key) {
if (key == 'isProxy')
return true;
const prop = target[key];
// return if property not found
if (typeof prop == 'undefined')
return;
// set value as proxy if object
if (!prop.isProxy && typeof prop === 'object')
target[key] = new Proxy(prop, handler);
return target[key];
},
set(target, key, value) {
console.log('Setting', target, `.${key} to equal`, value);
// todo : call callback
target[key] = value;
return true;
}
};
const test = {
string: "data",
number: 231321,
object: {
string: "data",
number: 32434
},
array: [
1, 2, 3, 4, 5
],
};
const proxy = new Proxy(test, handler);
console.log(proxy);
console.log(proxy.string); // "data"
proxy.string = "Hello";
console.log(proxy.string); // "Hello"
console.log(proxy.object); // { "string": "data", "number": 32434 }
proxy.object.string = "World";
console.log(proxy.object.string); // "World"
I published a library on GitHub that does this as well. It will also report to a callback function what modifications have taken place along with their full path.
Michal's answer is good, but it creates a new Proxy every time a nested object is accessed. Depending on your usage, that could lead to a very large memory overhead.
I have also created a library type function for observing updates on deeply nested proxy objects (I created it for use as a one-way bound data model). Compared to Elliot's library it's slightly easier to understand at < 100 lines. Moreover, I think Elliot's worry about new Proxy objects being made is a premature optimisation, so I kept that feature to make it simpler to reason about the function of the code.
observable-model.js
let ObservableModel = (function () {
/*
* observableValidation: This is a validation handler for the observable model construct.
* It allows objects to be created with deeply nested object hierarchies, each of which
* is a proxy implementing the observable validator. It uses markers to track the path an update to the object takes
* <path> is an array of values representing the breadcrumb trail of object properties up until the final get/set action
* <rootTarget> the earliest property in this <path> which contained an observers array *
*/
let observableValidation = {
get(target, prop) {
this.updateMarkers(target, prop);
if (target[prop] && typeof target[prop] === 'object') {
target[prop] = new Proxy(target[prop], observableValidation);
return new Proxy(target[prop], observableValidation);
} else {
return target[prop];
}
},
set(target, prop, value) {
this.updateMarkers(target, prop);
// user is attempting to update an entire observable field
// so maintain the observers array
target[prop] = this.path.length === 1 && prop !== 'length'
? Object.assign(value, { observers: target[prop].observers })
: value;
// don't send events on observer changes / magic length changes
if(!this.path.includes('observers') && prop !== 'length') {
this.rootTarget.observers.forEach(o => o.onEvent(this.path, value));
}
// reset the markers
this.rootTarget = undefined;
this.path.length = 0;
return true;
},
updateMarkers(target, prop) {
this.path.push(prop);
this.rootTarget = this.path.length === 1 && prop !== 'length'
? target[prop]
: target;
},
path: [],
set rootTarget(target) {
if(typeof target === 'undefined') {
this._rootTarget = undefined;
}
else if(!this._rootTarget && target.hasOwnProperty('observers')) {
this._rootTarget = Object.assign({}, target);
}
},
get rootTarget() {
return this._rootTarget;
}
};
/*
* create: Creates an object with keys governed by the fields array
* The value at each key is an object with an observers array
*/
function create(fields) {
let observableModel = {};
fields.forEach(f => observableModel[f] = { observers: [] });
return new Proxy(observableModel, observableValidation);
}
return {create: create};
})();
It's then trivial to create an observable model and register observers:
app.js
// give the create function a list of fields to convert into observables
let model = ObservableModel.create([
'profile',
'availableGames'
]);
// define the observer handler. it must have an onEvent function
// to handle events sent by the model
let profileObserver = {
onEvent(field, newValue) {
console.log(
'handling profile event: \n\tfield: %s\n\tnewValue: %s',
JSON.stringify(field),
JSON.stringify(newValue));
}
};
// register the observer on the profile field of the model
model.profile.observers.push(profileObserver);
// make a change to profile - the observer prints:
// handling profile event:
// field: ["profile"]
// newValue: {"name":{"first":"foo","last":"bar"},"observers":[{}
// ]}
model.profile = {name: {first: 'foo', last: 'bar'}};
// make a change to available games - no listeners are registered, so all
// it does is change the model, nothing else
model.availableGames['1234'] = {players: []};
Hope this is useful!
I wrote a function based on Michał Perłakowski code. I added access to the path of property in the set/get functions. Also, I added types.
const createHander = <T>(path: string[] = []) => ({
get: (target: T, key: keyof T): any => {
if (key == 'isProxy') return true;
if (typeof target[key] === 'object' && target[key] != null)
return new Proxy(
target[key],
createHander<any>([...path, key as string])
);
return target[key];
},
set: (target: T, key: keyof T, value: any) => {
console.log(`Setting ${[...path, key]} to: `, value);
target[key] = value;
return true;
}
});
const proxy = new Proxy(obj ,createHander<ObjectType>());

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