JS using dot, string notation to map nested and assign errors - javascript

Ok, this is an odd one that I just can't seem to get right.
I have a large, complex object to send to a backend. After various attempts, I tried using Joi to validate the schema, and I like it, but passing the errors back to the inputs is a nightmare
The body is subdivided into 5 sections, with each subsection containing between 10-30 fields, some of which are string[], interface[], number[], or general nested interfaces.
I tried writing my own custom validation and the complexity grew outta control.
(I know some of you are thinking "your schema is too complex" and you're right, but its not something I can change right now. Clients blah blah.)
The problem: Joi.validate(myBody) gives me a bunch of errors in the following format:
[ // <- error.details
{
context: {},
message: "X is not allowed to be empty",
path:["path","to","property"], // <- this is the key
type: ""
}
]
How can I map error.details to create a new validation object that I can then use for the form items themselves.
For example:
path = ["path","to","property"] // -> map over to create
let newObj = {
path:{
to: {
property: ""
}
}
}
I hope this make sense.
I want to take an array of vallidation errors, and turn them into a validation object that matches the initial object

The simplest approach IMO would be to use reduce to reverse create the object from the array
["path","to","property"].reduceRight((prev, current) => {
const obj = {};
obj[current] = prev
return obj;
}, "");
This will create the object as described in the original question. You need to use reduceRight rather than reduce so that you create the leaf node first otherwise you have having to try and traverse the graph each time you add a new node and you have to handle setting the leaf node to be a string rather than an object.
Extrapolating out what you are trying to achieve I'm assuming a couple of things:
You want to return a single object rather than an array of objects.
The leaf is likely to be the message.
There will only be a single error message for each path (because it makes life easier).
We can expand upon the above with the deep merge solution from here to create an object to return. The code for that would look like:
const errors = [ // <- error.details
{
context: {},
message: "X is not allowed to be empty",
path:["path","to","property"], // <- this is the key
type: ""
},
{
context: {},
message: "X has to be greater than 0",
path:["path","to","another", "property"], // <- this is the key
type: ""
}
]
function isObject(item) {
return (item && typeof item === 'object' && !Array.isArray(item));
}
function mergeDeep(target, ...sources) {
if (!sources.length) return target;
const source = sources.shift();
if (isObject(target) && isObject(source)) {
for (const key in source) {
if (isObject(source[key])) {
if (!target[key]) Object.assign(target, { [key]: {} });
mergeDeep(target[key], source[key]);
} else {
Object.assign(target, { [key]: source[key] });
}
}
}
return mergeDeep(target, ...sources);
}
errors.map((e) => e.path.reduceRight((prev, current) => {
const obj = {};
obj[current] = prev
return obj;
}, e.message)).reduce((previous, current) => mergeDeep(previous, current), {})
The output from running errors through it would be:
{
path: {
to: {
property: 'X is not allowed to be empty',
another: { property: 'X has to be greater than 0' }
}
}
}

Problem statement being used
I want to take an array of vallidation errors, and turn them into a validation object that matches the initial object
Given: an array of strings (each of which is a prop)
Expected result: an object structured based on the array
Code Snippet
// given an array of props
// starting at root-level & ending at the leaf
const pList = ['root', 'lev1', 'lev2', 'propName'];
// method to transform array into an object
const transformToObj = arr => (
arr
.reverse()
.reduce(
(fin, itm) => ({ [itm]: fin ? {...fin} : '' }),
false
)
);
// test with given array
console.log(transformToObj(pList));
// test with user-input array (comma-separated)
console.log(transformToObj([...(
(prompt('Enter comma-separated path like a,b,c,d'))
.split(',')
.map(x => x.trim())
)]));
Explanation
first reverse the array (so the first item is the inner-most prop)
use .reduce to iterate
at each level, add the item as the outer-prop and the value as the existing object
if this is the inner-most prop, simply add an empty string as value

Related

Trying to see if object key exists keep getting undefined

I have a data response that responds with different objects in an array.
I have tried a variety of different methods to determine if the key 'name' exists including:
const hasName = array.forEach(item => "name" in item )
const hasName = array.forEach(item => Object.keys(item).includes('name') )
const hasName = array[0].hasOwnProperty("name") ? true : null
const hasName = array => array.some(x => x.some(({ name }) => name));
// lodash 'has' method
const hasName = has(array, 'name')
Array1 returns objects like:
[
{
name: 'cool name'
}
]
Array2 returns objects like:
[
{
group: 'cool group'
}
]
All of the methods I tried have either returned undefined or nothing at all and I'm completely at a loss for why. I have scoured Stack Overflow and the internet trying to get a better direction or an answer but I haven't been able to find anything.
You're not returning anything from some of your calls on the array. forEach for example runs a callback, and always returns undefined see docs. Your code just isn't working because you're using the functions incorrectly.
The code below filters your array to get the elements with a name property, then counts them to see whether one or more exists, which results in a true or false being stored in the hasName variable.
let myArr = [
{
name: 'cool name'
}
]
const hasName =
myArr
.filter(a => a.hasOwnProperty("name")) // filter for elements in the array which have a name property
.length // get the number of filtered elements
> 0 // check whether the number of elements in array with name prop is more than 0
console.log(hasName)
If you are sure that the "response" is fully received before the check, THEN
Your latest variant of the check can be implemented as follows:
array.some(obj => (obj.name !== undefined)); // fast, but not define .name if it is "undefined"
array.some(obj => obj.hasOwnProperty("name")); // slower, but will define .name with any value
Your solutions are generally correct, but it looks like you're a little confused,
Array.forEach always returns "undefined":
array.forEach(obj => {
console.log("name" in obj); // true for 1st array
console.log("group" in obj); // true for 2nd array
}); // return "undefined"
array[0].hasOwnProperty() works fine, and can't return "undefined" at all.
console.log(
array[0].hasOwnProperty("name") ? true : null; // return ("true" for the 1st array) AND ("null" for the 2nd)
);
When you used the Lodash, maybe you forgot to point to the object index?
_.has(array[0], 'name'); // return ("true" for the 1st array) AND ("false" for the 2nd)
Try a for loop
for (var i = 0; i < array.length; i++) {
if (array[i].hasOwnProperty('name')) {
console.log(array[i].name); //do something here.
break;
}
}

Role hierarchy mapper / generator (recursive)

I would like to create an object of arrays converting the single level key - (string) value relation to key - (array) keys collection.
Basically, the code must collect other keys and their values recursively starting from collecting self. At the end the object must be like this;
{
ROLE_SUPER_ADMIN: [
'ROLE_SUPER_ADMIN',
'ROLE_ADMIN',
'ROLE_MODERATOR',
'ROLE_AUTHOR'
]
}
What i have achieved yet is;
export const roles = {
ROLE_SUPER_ADMIN: 'ROLE_ADMIN',
ROLE_ADMIN: 'ROLE_MODERATOR',
ROLE_MODERATOR: 'ROLE_AUTHOR',
ROLE_AUTHOR: null,
ROLE_CLIENT: null
}
export function roleMapper() {
const roleArray = {}
const mapper = (key) => {
roleArray[key] = [key];
if (!roles[key] || Array.isArray(roles[key])) {
return;
} else if (!roles[roles[key]]) {
roleArray[key].push(roles[key])
} else {
if (roleArray.hasOwnProperty(key)) {
Object.keys(roles).filter(r => r !== key).forEach((role) => {
roleArray[key].push(mapper(role))
})
}
}
}
Object.keys(roles).forEach((key) => {
mapper(key)
});
console.log(roleArray);
}
I have completely lost solving this. Please help, thanks.
I would use a function generator for this, taking advantage of the easy recursion approach and taking advantage of Object.entries combined with Array.map.
The below method acquires all the siblings of a defined key from an object, assuming that each key value may be the child of the said key.
As a side note, you could technically do that in many other ways (without relying on function generators), I just think that the generator approach is clever and easier to maintain. Moreover, it allows you to re-use the method later and allows you to eventually iterate the values.
Code explanation is directly in the code below.
const roles = {
ROLE_SUPER_ADMIN: 'ROLE_ADMIN',
ROLE_ADMIN: 'ROLE_MODERATOR',
ROLE_MODERATOR: 'ROLE_AUTHOR',
ROLE_AUTHOR: null,
ROLE_CLIENT: null
}
// Acquire all the siblings, where a sibling is a key whose value is the value of another key.
function* getSiblings(v, source) {
// if the desired key exists in source..
if (source[v]) {
// yield the value, which is a role in that case.
yield source[v];
// next, yield all the siblings of that value (role).
yield* [...getSiblings(source[v], source)];
}
}
// Map all roles by its siblings.
const res = Object.entries(roles).map(([key, role]) => {
// key is the main role, whereas role is the "child" role.
// Technically, [key] is not exactly a child role of [key], so we're injecting it manually below to avoid polluting the getSiblings method.
return {
[key]: [key, ...getSiblings(key, roles)] // <-- as mentioned above, the array is build by starting from the main role (key) and appending the child roles (siblings). [key] is a shorthand to set the key.
}
});
console.log(res);
I would separate out the recursive call necessary to fetch the list from the code that builds the output. That allows you to make both of them quite simple:
const listRoles = (rolls, name) => name in roles
? [name, ... listRoles (rolls, roles [name] )]
: []
const roleMapper = (roles) => Object .assign (
... Object.keys (roles) .map (name => ({ [name]: listRoles (roles, name) }))
)
const roles = {ROLE_SUPER_ADMIN: 'ROLE_ADMIN', ROLE_ADMIN: 'ROLE_MODERATOR', ROLE_MODERATOR: 'ROLE_AUTHOR', ROLE_AUTHOR: null, ROLE_CLIENT: null}
console .log (
roleMapper (roles)
)
Here listRoles is the recursive bit, and it simply takes a roles object and a name and returns all the descendant names, so
listRoles(roles, 'ROLE_MODERATOR') //=> ["ROLE_MODERATOR", "ROLE_AUTHOR"]
roleMapper uses that function. It takes the roles object and calls listRoles on each of its keys, combining them into a new object.
Together, these yield the following output:
{
ROLE_SUPER_ADMIN: ["ROLE_SUPER_ADMIN", "ROLE_ADMIN", "ROLE_MODERATOR", "ROLE_AUTHOR"],
ROLE_ADMIN: ["ROLE_ADMIN", "ROLE_MODERATOR", "ROLE_AUTHOR"],
ROLE_MODERATOR: ["ROLE_MODERATOR", "ROLE_AUTHOR"],
ROLE_AUTHOR: ["ROLE_AUTHOR"],
ROLE_CLIENT: ["ROLE_CLIENT"]
}
I see the accepted answer generates a structure more like this:
[
{ROLE_SUPER_ADMIN: ["ROLE_SUPER_ADMIN", "ROLE_ADMIN", "ROLE_MODERATOR", "ROLE_AUTHOR"]},
{ROLE_ADMIN: ["ROLE_ADMIN", "ROLE_MODERATOR", "ROLE_AUTHOR"]},
{ROLE_MODERATOR: ["ROLE_MODERATOR", "ROLE_AUTHOR"]},
{ROLE_AUTHOR: ["ROLE_AUTHOR"]},
{ROLE_CLIENT: ["ROLE_CLIENT"]}
]
(The difference is that mine was a single object, versus this one which was an array of single-property objects.)
While that feels less logical to me, it would be even easier to write:
const roleMapper = (roles) => Object.keys (roles) .map (n => ({ [n]: listRoles (roles, n) }))

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

Functional Javascript - Convert to dotted format in FP way (uses Ramda)

I am learning functional programming in Javascript and using Ramda. I have this object
var fieldvalues = { name: "hello there", mobile: "1234",
meta: {status: "new"},
comments: [ {user: "john", comment: "hi"},
{user:"ram", comment: "hello"}]
};
to be converted like this:
{
comments.0.comment: "hi",
comments.0.user: "john",
comments.1.comment: "hello",
comments.1.user: "ram",
meta.status: "new",
mobile: "1234",
name: "hello there"
}
I have tried this Ramda source, which works.
var _toDotted = function(acc, obj) {
var key = obj[0], val = obj[1];
if(typeof(val) != "object") { // Matching name, mobile etc
acc[key] = val;
return acc;
}
if(!Array.isArray(val)) { // Matching meta
for(var k in val)
acc[key + "." + k] = val[k];
return acc;
}
// Matching comments
for(var idx in val) {
for(var k2 in val[idx]) {
acc[key + "." + idx + "." + k2] = val[idx][k2];
}
}
return acc;
};
// var toDotted = R.pipe(R.toPairs, R.reduce(_toDotted, {}));
var toDotted = R.pipe(R.toPairs, R.curry( function(obj) {
return R.reduce(_toDotted, {}, obj);
}));
console.log(toDotted(fieldvalues));
However, I am not sure if this is close to Functional programming methods. It just seems to be wrapped around some functional code.
Any ideas or pointers, where I can make this more functional way of writing this code.
The code snippet available here.
UPDATE 1
Updated the code to solve a problem, where the old data was getting tagged along.
Thanks
A functional approach would
use recursion to deal with arbitrarily shaped data
use multiple tiny functions as building blocks
use pattern matching on the data to choose the computation on a case-by-case basis
Whether you pass through a mutable object as an accumulator (for performance) or copy properties around (for purity) doesn't really matter, as long as the end result (on your public API) is immutable. Actually there's a nice third way that you already used: association lists (key-value pairs), which will simplify dealing with the object structure in Ramda.
const primitive = (keys, val) => [R.pair(keys.join("."), val)];
const array = (keys, arr) => R.addIndex(R.chain)((v, i) => dot(R.append(keys, i), v), arr);
const object = (keys, obj) => R.chain(([v, k]) => dot(R.append(keys, k), v), R.toPairs(obj));
const dot = (keys, val) =>
(Object(val) !== val
? primitive
: Array.isArray(val)
? array
: object
)(keys, val);
const toDotted = x => R.fromPairs(dot([], x))
Alternatively to concatenating the keys and passing them as arguments, you can also map R.prepend(key) over the result of each dot call.
Your solution is hard-coded to have inherent knowledge of the data structure (the nested for loops). A better solution would know nothing about the input data and still give you the expected result.
Either way, this is a pretty weird problem, but I was particularly bored so I figured I'd give it a shot. I mostly find this a completely pointless exercise because I cannot picture a scenario where the expected output could ever be better than the input.
This isn't a Rambda solution because there's no reason for it to be. You should understand the solution as a simple recursive procedure. If you can understand it, converting it to a sugary Rambda solution is trivial.
// determine if input is object
const isObject = x=> Object(x) === x
// flatten object
const oflatten = (data) => {
let loop = (namespace, acc, data) => {
if (Array.isArray(data))
data.forEach((v,k)=>
loop(namespace.concat([k]), acc, v))
else if (isObject(data))
Object.keys(data).forEach(k=>
loop(namespace.concat([k]), acc, data[k]))
else
Object.assign(acc, {[namespace.join('.')]: data})
return acc
}
return loop([], {}, data)
}
// example data
var fieldvalues = {
name: "hello there",
mobile: "1234",
meta: {status: "new"},
comments: [
{user: "john", comment: "hi"},
{user: "ram", comment: "hello"}
]
}
// show me the money ...
console.log(oflatten(fieldvalues))
Total function
oflatten is reasonably robust and will work on any input. Even when the input is an array, a primitive value, or undefined. You can be certain you will always get an object as output.
// array input example
console.log(oflatten(['a', 'b', 'c']))
// {
// "0": "a",
// "1": "b",
// "2": "c"
// }
// primitive value example
console.log(oflatten(5))
// {
// "": 5
// }
// undefined example
console.log(oflatten())
// {
// "": undefined
// }
How it works …
It takes an input of any kind, then …
It starts the loop with two state variables: namespace and acc . acc is your return value and is always initialized with an empty object {}. And namespace keeps track of the nesting keys and is always initialized with an empty array, []
notice I don't use a String to namespace the key because a root namespace of '' prepended to any key will always be .somekey. That is not the case when you use a root namespace of [].
Using the same example, [].concat(['somekey']).join('.') will give you the proper key, 'somekey'.
Similarly, ['meta'].concat(['status']).join('.') will give you 'meta.status'. See? Using an array for the key computation will make this a lot easier.
The loop has a third parameter, data, the current value we are processing. The first loop iteration will always be the original input
We do a simple case analysis on data's type. This is necessary because JavaScript doesn't have pattern matching. Just because were using a if/else doesn't mean it's not functional paradigm.
If data is an Array, we want to iterate through the array, and recursively call loop on each of the child values. We pass along the value's key as namespace.concat([k]) which will become the new namespace for the nested call. Notice, that nothing gets assigned to acc at this point. We only want to assign to acc when we have reached a value and until then, we're just building up the namespace.
If the data is an Object, we iterate through it just like we did with an Array. There's a separate case analysis for this because the looping syntax for objects is slightly different than arrays. Otherwise, it's doing the exact same thing.
If the data is neither an Array or an Object, we've reached a value. At this point we can assign the data value to the acc using the built up namespace as the key. Because we're done building the namespace for this key, all we have to do compute the final key is namespace.join('.') and everything works out.
The resulting object will always have as many pairs as values that were found in the original object.

Design pattern to check if a JavaScript object has changed

I get from the server a list of objects
[{name:'test01', age:10},{name:'test02', age:20},{name:'test03', age:30}]
I load them into html controls for the user to edit.
Then there is a button to bulk save the entire list back to the database.
Instead of sending the whole list I only want to send the subset of objects that were changed.
It can be any number of items in the array. I want to do something similar to frameworks like Angular that mark an object property like "pristine" when no change has been done to it. Then use that flag to only post to the server the items that are not "pristine", the ones that were modified.
Here is a function down below that will return an array/object of changed objects when supplied with an old array/object of objects and a new array of objects:
// intended to compare objects of identical shape; ideally static.
//
// any top-level key with a primitive value which exists in `previous` but not
// in `current` returns `undefined` while vice versa yields a diff.
//
// in general, the input type determines the output type. that is if `previous`
// and `current` are objects then an object is returned. if arrays then an array
// is returned, etc.
const getChanges = (previous, current) => {
if (isPrimitive(previous) && isPrimitive(current)) {
if (previous === current) {
return "";
}
return current;
}
if (isObject(previous) && isObject(current)) {
const diff = getChanges(Object.entries(previous), Object.entries(current));
return diff.reduce((merged, [key, value]) => {
return {
...merged,
[key]: value
}
}, {});
}
const changes = [];
if (JSON.stringify(previous) === JSON.stringify(current)) {
return changes;
}
for (let i = 0; i < current.length; i++) {
const item = current[i];
if (JSON.stringify(item) !== JSON.stringify(previous[i])) {
changes.push(item);
}
}
return changes;
};
For Example:
const arr1 = [1, 2, 3, 4]
const arr2 = [4, 4, 2, 4]
console.log(getChanges(arr1, arr2)) // [4,4,2]
const obj1 = {
foo: "bar",
baz: [
1, 2, 3
],
qux: {
hello: "world"
},
bingo: "name-o",
}
const obj2 = {
foo: "barx",
baz: [
1, 2, 3, 4
],
qux: {
hello: null
},
bingo: "name-o",
}
console.log(getChanges(obj1.foo, obj2.foo)) // barx
console.log(getChanges(obj1.bingo, obj2.bingo)) // ""
console.log(getChanges(obj1.baz, obj2.baz)) // [4]
console.log(getChanges(obj1, obj2)) // {foo:'barx',baz:[1,2,3,4],qux:{hello:null}}
const obj3 = [{ name: 'test01', age: 10 }, { name: 'test02', age: 20 }, { name: 'test03', age: 30 }]
const obj4 = [{ name: 'test01', age: 10 }, { name: 'test02', age: 20 }, { name: 'test03', age: 20 }]
console.log(getChanges(obj3, obj4)) // [{name:'test03', age:20}]
Utility functions used:
// not required for this example but aid readability of the main function
const typeOf = o => Object.prototype.toString.call(o);
const isObject = o => o !== null && !Array.isArray(o) && typeOf(o).split(" ")[1].slice(0, -1) === "Object";
const isPrimitive = o => {
switch (typeof o) {
case "object": {
return false;
}
case "function": {
return false;
}
default: {
return true;
}
}
};
You would simply have to export the full list of edited values client side, compare it with the old list, and then send the list of changes off to the server.
Hope this helps!
Here are a few ideas.
Use a framework. You spoke of Angular.
Use Proxies, though Internet Explorer has no support for it.
Instead of using classic properties, maybe use Object.defineProperty's set/get to achieve some kind of change tracking.
Use getter/setting functions to store data instead of properties: getName() and setName() for example. Though this the older way of doing what defineProperty now does.
Whenever you bind your data to your form elements, set a special property that indicates if the property has changed. Something like __hasChanged. Set to true if any property on the object changes.
The old school bruteforce way: keep your original list of data that came from the server, deep copy it into another list, bind your form controls to the new list, then when the user clicks submit, compare the objects in the original list to the objects in the new list, plucking out the changed ones as you go. Probably the easiest, but not necessarily the cleanest.
A different take on #6: Attach a special property to each object that always returns the original version of the object:
var myData = [{name: "Larry", age: 47}];
var dataWithCopyOfSelf = myData.map(function(data) {
Object.assign({}, data, { original: data });
});
// now bind your form to dataWithCopyOfSelf.
Of course, this solution assumes a few things: (1) that your objects are flat and simple since Object.assign() doesn't deep copy, (2) that your original data set will never be changed, and (3) that nothing ever touches the contents of original.
There are a multitude of solutions out there.
With ES6 we can use Proxy
to accomplish this task: intercept an Object write, and mark it as dirty.
Proxy allows to create a handler Object that can trap, manipulate, and than forward changes to the original target Object, basically allowing to reconfigure its behavior.
The trap we're going to adopt to intercept Object writes is the handler set().
At this point we can add a non-enumerable property flag like i.e: _isDirty using Object.defineProperty() to mark our Object as modified, dirty.
When using traps (in our case the handler's set()) no changes are applied nor reflected to the Objects, therefore we need to forward the argument values to the target Object using Reflect.set().
Finally, to retrieve the modified objects, filter() the Array with our proxy Objects in search of those having its own Property "_isDirty".
// From server:
const dataOrg = [
{id:1, name:'a', age:10},
{id:2, name:'b', age:20},
{id:3, name:'c', age:30}
];
// Mirror data from server to observable Proxies:
const data = dataOrg.map(ob => new Proxy(ob, {
set() {
Object.defineProperty(ob, "_isDirty", {value: true}); // Flag
return Reflect.set(...arguments); // Forward trapped args to ob
}
}));
// From now on, use proxied data. Let's change some values:
data[0].name = "Lorem";
data[0].age = 42;
data[2].age = 31;
// Collect modified data
const dataMod = data.filter(ob => ob.hasOwnProperty("_isDirty"));
// Test what we're about to send back to server:
console.log(JSON.stringify(dataMod, null, 2));
Without using .defineProperty()
If for some reason you don't feel comfortable into tapping into the original object adding extra properties as flags, you could instead populate immediately
the dataMod (array with modified Objects) with references:
const dataOrg = [
{id:1, name:'a', age:10},
{id:2, name:'b', age:20},
{id:3, name:'c', age:30}
];
// Prepare array to hold references to the modified Objects
const dataMod = [];
const data = dataOrg.map(ob => new Proxy(ob, {
set() {
if (dataMod.indexOf(ob) < 0) dataMod.push(ob); // Push reference
return Reflect.set(...arguments);
}
}));
data[0].name = "Lorem";
data[0].age = 42;
data[2].age = 31;
console.log(JSON.stringify(dataMod, null, 2));
Can I Use - Proxy (IE)
Proxy - handler.set()
Global Objects - Reflect
Reflect.set()
Object.defineProperty()
Object.hasOwnProperty()
Without having to get fancy with prototype properties you could simply store them in another array whenever your form control element detects a change
Something along the lines of:
var modified = [];
data.forEach(function(item){
var domNode = // whatever you use to match data to form control element
domNode.addEventListener('input',function(){
if(modified.indexOf(item) === -1){
modified.push(item);
}
});
});
Then send the modified array to server when it's time to save
Why not use Ember.js observable properties ? You can use the Ember.observer function to get and set changes in your data.
Ember.Object.extend({
valueObserver: Ember.observer('value', function(sender, key, value, rev) {
// Executes whenever the "value" property changes
// See the addObserver method for more information about the callback arguments
})
});
The Ember.object actually does a lot of heavy lifting for you.
Once you define your object, add an observer like so:
object.addObserver('propertyKey', targetObject, targetAction)
My idea is to sort object keys and convert object to be string to compare:
// use this function to sort keys, and save key=>value in an array
function objectSerilize(obj) {
let keys = Object.keys(obj)
let results = []
keys.sort((a, b) => a > b ? -1 : a < b ? 1 : 0)
keys.forEach(key => {
let value = obj[key]
if (typeof value === 'object') {
value = objectSerilize(value)
}
results.push({
key,
value,
})
})
return results
}
// use this function to compare
function compareObject(a, b) {
let aStr = JSON.stringify(objectSerilize(a))
let bStr = JSON.stringify(objectSerilize(b))
return aStr === bStr
}
This is what I think up.
It would be cleanest, I’d think to have the object emit an event when a property is added or removed or modified.
A simplistic implementation could involve an array with the object keys; whenever a setter or heck the constructor returns this, it first calls a static function returning a promise; resolving: map with changed values in the array: things added, things removed, or neither. So one could get(‘changed’) or so forth; returning an array.
Similarly every setter can emit an event with arguments for initial value and new value.
Assuming classes are used, you could easily have a static method in a parent generic class that can be called through its constructor and so really you could simplify most of this by passing the object either to itself, or to the parent through super(checkMeProperty).

Categories

Resources