Iterate through a specific nested object key - javascript

There is an object like:
props = {
any: 'thing',
intro: { content: 'foo' }
}
Now I want to loop through a given string representing a specific path (props.intro.content) in a way to set the deepest value to undefined:
props.intro.content = undefined
props.intro = undefined
props = undefined
So the results by iterating the path given above should output these three objects:
{
any: 'thing',
intro: { content: undefined }
},
{
any: 'thing',
intro: undefined
},
undefined
I tried to use split and for loop
const array = 'props.intro.content'.split('.')
for (let index = array.length - 1; index > -1; index--) {
console.log([array.join('.')]) // this returns the flatten path
array.pop()
}
but this doesn't handle the object itself, so I do not get the correct object as output.

Here's a recursive function that does that. I made the example extra deep to show that it's robust to deep nesting.
The one tricky part is the last one you should get just undefined if the path starts with the variables name. You can't get the name of the variable referencing the object you pass into the function, so maybe you can add a boolean parameter that pushes undefined to the end of the array, and have the string input start at the first key layer.
const object = {
any: 'thing',
intro: {
content: {
lets: {
go: {
deeper: 20
}
}
}
}
}
function deepDelete (mainObj, locations) {
const props = locations.split('.')
const outputs = [];
function recurseDelete(obj, props, mainObj) {
if (props.length === 0) return
recurseDelete(obj[props[0]], props.slice(1), mainObj)
obj[props[0]] = undefined
outputs.push(structuredClone(mainObj))
}
recurseDelete(mainObj, props, mainObj);
return outputs
}
const locations = 'intro.content.lets.go.deeper';
const outputArray = deepDelete(object, locations)
console.log(outputArray)

Related

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

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

cannot set property of undefined in a loop

I'm doing a loop to generate array of object. I have used faker to generate fake name, but in specified iteration I want to insert my own value, in this case the person's car.
Below code I got cannot set property of car of undefined, what's wrong?
const person = []
times(2, index => {
if (index === 0) {
person[0].car = 'honda'
} else if(index ===1) {
person[1].car = 'ford'
}
person.push({
name: faker.random.name()
})
}
console.log(person)
Apart from some closing parentheses missing, you try to first access an unexisting object (person[0].car...), and create it later (person.push(...)).
Reverse the order.
let newPerson = { name: faker.random.name(), car: 'dodge' };
person.push(newPerson);
person is an empty array initially, there are no objects in it, so when trying to do person[0].car it throws error because person[0] is undefined and and you cannot set or get any property from undefined.
Instead do this
person[0] = {}
person.car = 'honda'
or
person[0] = {'car': 'honda'}

Access multi-level property and its full path of properties

I'm currently struggling with a JavaScript problem. I want to return a multi-level property, as well as every variable contained within, by passing in the original object, and an array of paths to the properties I want.
For example, if I have the following object:
obj = {
product: {
candidate: {
id: 10,
reference: "test",
count: 4,
steps: 10
}
}
}
I want to be able to call a method:
getVarPath(obj, ["product.candidate.ID", "product.candidate.reference"])
And then have it return one object with each variable passed in the array, in it's original structure. So this would return an object looking like so:
{
product: {
candidate: {
id: 10,
reference: "test"
}
}
}
I do have this working in my local solution at the moment (passing in one string rather than an array at the moment).
The solution at the moment is pretty horrid but I'm looking to improve it, so if anyone can think of a better method that would be great.
Again, this is pretty horrid right now but I'm looking to improve it. But it does the job:
var getVarPath = function(obj, keys){
var elements = keys.split("."),
evalStr = "",
objStr = "obj",
newObjStr = "newObj",
newObj = {};
if(elements.length > 1){
elements.forEach(function(key, index){
// first append a property accessor at the end of the eval string
evalStr = evalStr + "['" + key + "']";
// if we're at the last element, we've reached the value, so assign it
if(index === elements.length -1){
eval(newObjStr + evalStr + " = " + objStr + evalStr);
}
else {
// if we're not at the last, we're at an object level
// if the nested object doesn't exist yet, create it
if(!eval(newObjStr + evalStr)){
eval(newObjStr + evalStr + " = {};");
}
}
});
}
return newObj;
}
For each element in the input array:
First, you can split the initial string: var nestedElements="product.candidate.ID".split(.)"
This returns an array with each level: ["product","candidate","ID"]
Now you can access to your nested object using each element of the array: obj["product"]["candidate"]["ID"] either by using a loop over the array or recursion.
var currentobj=obj;
for (var i=0;i<nestedElements.length;i++){
currentobj=currentobj[nestedElements[i]]
}
// currentobj is your id
In the same process, you could dynamically add elements to a new obj using a similar process:
newobj={} //before loop
if (newobj["product"] === undefined) newobj["product"]={} //in loop
And that should be done for each element on the input array, in the end is iterating through arrays and accessing the object using strings
Your code as-is shouldn't actually work. You're treating keys as a string, but passing in an array. You can (and should) avoid using eval(), by keeping track of the "inner" objects you're currently looking at, and using object[property] notation instead of object.property.
function getVarPath(obj, keys) {
var result = {};
// ["product.candidate.id", "product.candidate.reference"]
keys.forEach(function(key) {
var src = obj, // inner source object
dest = result, // inner destination object
parts = key.split(/\./);
// e.g. ["product", "candidate", "id"]
parts.forEach(function(part) {
// if we're looking at an object, make sure it exists in the dest
if (typeof(src[part]) === "object")
dest[part] = dest[part] || {};
// if it's just a value, copy it
else
dest[part] = src[part];
dest = dest[part]; // move from obj to obj.product, then to obj.product.candidate, etc.
src = src[part];
});
});
return result;
}
var obj = {
product: {
candidate: {
id: 10,
reference: "test",
count: 4,
steps: 10
}
}
}
var output = getVarPath(obj, ["product.candidate.id", "product.candidate.reference"]);
console.log(JSON.stringify(output));
Using _.propertyOf(), Array#reduce(), and Object.assign(), as well as computed property names, you could create a less daunting implementation:
function getVarPath(object, paths) {
return paths.reduce(function (accumulator, path) {
const that = _.propertyOf(accumulator)
let walk = path.split('.')
let value = this(walk)
for (let key = walk.pop(); key !== undefined; key = walk.pop()) {
const base = that(walk)
value = { [key]: value }
if (base !== undefined) {
value = Object.assign(base, value)
}
}
return Object.assign(accumulator, value)
}.bind(_.propertyOf(object)), {})
}
let obj = {
product: {
candidate: {
id: 10,
reference: "test",
count: 4,
steps: 10
}
}
}
console.log(getVarPath(obj, ['product.candidate.id', 'product.candidate.reference']))
<script src="https://cdn.rawgit.com/lodash/lodash/4.17.4/dist/lodash.min.js"></script>

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).

How can I find the string value of a property given it's value

I created something similar to an ENUM like this:
var ContentStatusId = {
All: 0,
Production: 1,
Review: 2,
Draft: 3,
Concept: 4
}
so when I set:
var a = ContentStatusId.All
it gets the value of 0
How can I go in the other direction? If I know a = 0 and a comes from ContentStatusId then how can I get the string "All"?
Iterate over the properties until you find the one with the value you want
function findVal(obj, val) {
for (var prop in obj) {
if (obj.hasOwnProperty(prop)) { // skip inherited properties
if (obj[prop] == val) {
return prop;
}
}
}
return false;
}
You'll have to iterate through the properties and check whether they equal the value on each iteration:
for (var property in ContentStatusId) {
if (ContentStatusId.hasOwnProperty(property)) {
if (ContentStatusId[property] == /*value to look for*/) {
console.log(property);
}
}
}
Demo
You can't do this directly. You'd have to store the key somewhere...
var a = ['All', ContentStatusId.All];
or, more advanced:
var a = {
source: ContentStatusId,
key: 'All',
update: function () {
this.value = this.source[this.key];
}
};
a.update();
Depending on what you have to do, you can either iterate over the object or if you have to do this more often, just create an array where you can do your reverse-lookup:
var ContentStatusIdReverse = ["All","Production",...]
ContentStatusIdReverse[0] // Yields "All"
You can create this array in the first place by iterating over your object once, and all consecutive lookups can be done via the array.

Categories

Resources