Reputation: 468
I am trying to find a way to dynamically create an array that contains the paths of the deepest properties in a nested object. For example, if my object is the following:
{
userName: [],
email: [],
name: {
fullName: [],
split: {
first: [],
last: []
}
},
date: {
input: {
split: {
month: [],
year: []
},
full: []
},
select: {
month: [],
year: []
}
}
};
I would need an array to contain something like:
["userName", "email", "name.fullName", "name.split.first",...]
Are there any built-in or external libraries that do this automatically? I was trying to use Object.keys on the parent object but this only returns the direct children properties.
Upvotes: 0
Views: 762
Reputation: 135357
You can use Array.prototype.flatMap
for this -
const d =
{userName:[],email:[],name:{fullName:[],split:{first:[],last:[]}},date:{input:{split:{month:[],year:[]},full:[]},select:{month:[],year:[]}}}
const main = (o = {}, path = []) =>
Array.isArray(o) || Object(o) !== o
? [ path ]
: Object
.entries(o)
.flatMap(([ k, v ]) => main(v, [...path, k ]))
console.log(main(d))
Output
[ [ "userName" ]
, [ "email" ]
, [ "name", "fullName" ]
, [ "name" ,"split", "first" ]
, [ "name", "split", "last" ]
, ...
]
If you want the paths to be "a.b.c"
instead of [ "a", "b", "c" ]
, use .map
and Array.prototype.join
-
const d =
{userName:[],email:[],name:{fullName:[],split:{first:[],last:[]}},date:{input:{split:{month:[],year:[]},full:[]},select:{month:[],year:[]}}}
const main = (o = {}, path = []) =>
Array.isArray(o) || Object(o) !== o
? [ path ]
: Object
.entries(o)
.flatMap(([ k, v ]) => main(v, [...path, k ]))
console.log(main(d).map(path => path.join(".")))
Output
[
"userName",
"email",
"name.fullName",
"name.split.first",
"name.split.last",
"date.input.split.month",
"date.input.split.year",
"date.input.full",
"date.select.month",
"date.select.year"
]
If you do not want to rely on Array.prototype.flatMap
because it is not supported in your environment, you can use a combination of Array.prototype.reduce
and Array.prototype.concat
-
const d =
{userName:[],email:[],name:{fullName:[],split:{first:[],last:[]}},date:{input:{split:{month:[],year:[]},full:[]},select:{month:[],year:[]}}}
const main = (o = {}, path = []) =>
Array.isArray(o) || Object(o) !== o
? [ path ]
: Object
.entries(o)
.reduce // <-- manual flatMap
( (r, [ k, v ]) =>
r.concat(main(v, [...path, k ]))
, []
)
console.log(main(d).map(path => path.join(".")))
Or you could polyfill Array.prototype.flatMap
-
Array.prototype.flatMap = function (f, context)
{ return this.reduce
( (r, x, i, a) => r.concat(f.call(context, r, x, i, a))
, []
)
}
Is there a way to access any of those properties' value? Eg.
"d.name.split.first"
using the returned array at position 3?
We can write a lookup function that accepts an object, o
, and a dot-separated string, s
, that returns a value, if possible, otherwise returns undefined
if s
is unreachable -
const d =
{userName:[],email:[],name:{fullName:[],split:{first:[],last:[]}},date:{input:{split:{month:[],year:[]},full:[]},select:{month:[],year:[]}}}
const lookup = (o = {}, s = "") =>
s
.split(".")
.reduce
( (r, x) =>
r == null ? undefined : r[x]
, o
)
console.log(lookup(d, "name.split"))
// { first: [], last: [] }
console.log(lookup(d, "name.split.first"))
// []
console.log(lookup(d, "name.split.zzz"))
// undefined
console.log(lookup(d, "zzz"))
// undefined
Upvotes: 2
Reputation: 5082
come on. Arrays and Objects are basically the same thing. There is absolutely no need for undefined checks.
data = { ... };
function paths_list( value, result=[], path=[] )
{
for ( keydx in value )
{
if ( value[keydx] instanceof Object )
{
path.push( keydx );
result.push( path.join(".") );
paths_list( value[keydx], result, path );
path.pop();
}
}
return result;
}
console.log( paths_list(data) );
Prints
Array ["userName", "email", "name", "name.fullName", "name.split", "name.split.first", "name.split.last", "date", "date.input", "date.input.split", "date.input.split.month", "date.input.split.year", "date.input.full", "date.select", "date.select.month", "date.select.year"]
Upvotes: 1
Reputation: 207527
There are many ways to do it. Easiest is just a recursion to test if you have an Object and loop over the keys keeping track of the path on each step.
var myObj = {
userName: [],
email: [],
name: {
fullName: [],
split: {
first: [],
last: []
}
},
date: {
input: {
split: {
month: [],
year: []
},
full: []
},
select: {
month: [],
year: []
}
}
}
function setPath(a, b) {
return a.length ? a + '.' + b : b
}
function getAllPaths(obj, paths, currentPath) {
if (paths===undefined) paths = []
if (currentPath===undefined) currentPath = ''
Object.entries(obj).forEach( function (entry) {
const updatedPath = setPath(currentPath, entry[0])
if (entry[1] instanceof Object && !Array.isArray(entry[1])) {
getAllPaths(entry[1], paths, updatedPath)
} else {
paths.push(updatedPath)
}
})
return paths
}
console.log(getAllPaths(myObj))
written with arrow functions and default values
var myObj = {
userName: [],
email: [],
name: {
fullName: [],
split: {
first: [],
last: []
}
},
date: {
input: {
split: {
month: [],
year: []
},
full: []
},
select: {
month: [],
year: []
}
}
}
const setPath = (a, b) => a.length ? a + '.' + b : b
const getAllPaths = (obj, paths=[], currentPath='') => {
Object.entries(obj).forEach( ([key, value]) => {
const updatedPath = setPath(currentPath, key)
if (value instanceof Object && !Array.isArray(value)) {
getAllPaths(value, paths, updatedPath)
} else {
paths.push(updatedPath)
}
})
return paths
}
console.log(getAllPaths(myObj))
Upvotes: 1