I'm using an angular material tree to display a deeply nested object. While building the tree, how do I store the current path along with the values?
const TREE_DATA = JSON.parse(JSON.stringify({
"cars": [
{
"model": "",
"make": "Audi",
"year": ""
},
{
"model": "A8",
"make": "",
"year": "2007"
}
],
"toys": {
"color": "Black",
"type": [
{
"brand": "",
"price": "$100"
}
]
},
"id": "xyz",
"books": [
{
"publisher": [
{
"authors": []
}
]
}
],
"extra": "test"
}));
@Injectable()
export class FileDatabase {
dataChange = new BehaviorSubject<FileNode[]>([]);
get data(): FileNode[] { return this.dataChange.value; }
constructor() {
this.initialize();
}
initialize() {
const dataObject = JSON.parse(TREE_DATA);
const data = this.buildFileTree(dataObject, 0);
this.dataChange.next(data);
}
buildFileTree(obj: {[key: string]: any}, level: number): FileNode[] {
return Object.keys(obj).reduce<FileNode[]>((accumulator, key) => {
const value = obj[key];
const node = new FileNode();
node.filename = key;
if (value != null) {
if (typeof value === 'object') {
node.children = this.buildFileTree(value, level 1);
} else {
node.type = value;
}
}
return accumulator.concat(node);
}, []);
}
}
Currently, the buildFileTree function returns:
[
{
"filename": "cars",
"children": [
{
"filename": "0",
"children": [
{
"filename": "model",
"type": ""
},
{
"filename": "make",
"type": "Audi"
},
{
"filename": "year",
"type": ""
}
]
},
{
"filename": "1",
"children": [
{
"filename": "model",
"type": "A8"
},
{
"filename": "make",
"type": ""
},
{
"filename": "year",
"type": "2007"
}
]
}
]
},
{
"filename": "toys",
"children": [
{
"filename": "color",
"type": "Black"
},
{
"filename": "type",
"children": [
{
"filename": "0",
"children": [
{
"filename": "brand",
"type": ""
},
{
"filename": "price",
"type": "$100"
}
]
}
]
}
]
},
{
"filename": "id",
"type": "a"
},
{
"filename": "books",
"children": [
{
"filename": "0",
"children": [
{
"filename": "publisher",
"children": [
{
"filename": "0",
"children": [
{
"filename": "authors",
"type": []
}
]
}
]
}
]
}
]
},
{
"filename": "extra",
"type": "test"
}
]
While building this tree, how can I add the path to every "type" at every level? Something like "path": "cars.0.model" for the first "type" and so on.
CodePudding user response:
Try this with your buildFileTree
function
buildFileTree(obj: { [key: string]: any }, path?: string): FileNode[] {
return Object.keys(obj).reduce<FileNode[]>((accumulator, key) => {
const value = obj[key];
const node = new FileNode();
node.filename = key;
// Give the path a default value of '', or add a "." if it already has a value
path = path ? path '.' : '';
// Save this path to the node, and add the current key value to it
node.path = path key;
if (value != null) {
if (typeof value === 'object') {
// Pass the node.path value back to the buildFileTree function
node.children = this.buildFileTree(value, node.path);
} else {
node.type = value;
}
}
return accumulator.concat(node);
}, []);
}
I just added the path value, and passed it back through to the function on each loop.
I also removed the level
parameter, as you did not seem to be using it in the function.
CodePudding user response:
I would write a somewhat different version of buildFileTree
first, then layer the path-generation into that.
I would write the equivalent of what you have so far (ignoring the unused length
parameter) like this:
const buildFileTree = (o) =>
Object .entries (o) .map (([filename, type]) =>
Object (type) === type
? {filename, children: buildFileTree (type)}
: {filename, type}
)
And most likely, I would replace Object (type) === type
with a call to a suitable helper, isObject (type)
. isObject
is easily enough written or found.1
Then we can just add a defaulted path
parameter, update it and pass it through on recursive calls and include it in our output:
const concatPath = (s1) => (s2) =>
s1 ? s1 '.' s2 : s2
const buildFileTree = (o, path = '') =>
Object .entries (o) .map (([filename, type]) =>
Object (type) === type
? {filename, path: concatPath (path) (filename), children: buildFileTree (type, concatPath (path) (filename))}
: {filename, path: concatPath (path) (filename), type}
)
But even with concatPath
extracted to a helper, this has some unfortunate duplication. We call concatPath
in three places in the code with the same parameters, and in the recursive case, we use two of them. There are two different techniques that I use to solve this, and a third, more common one that I prefer not to use. That third one would be just to make a local variable and use it in the three places we now call concatPath
:
const buildFileTree = (o, p = '') =>
Object .entries (o) .map (([filename, type]) => {
const path = p ? p '.' filename : filename
return Object (type) === type
? {filename, path, children: buildFileTree (type, path)}
: {filename, path, type}
})
There's nothing wrong with this. But my preferred style is to minimize local variables and to try to work with only expressions and not statements. My two techniques are similar to one another. The first one would be to add a defaulted parameter to the map
call. The unfortunate problem with this is that the callback there already supplies two additional parameters after the item (the index and the whole array) and so we need some slight gymnastics adding dummy parameters in their place:
const buildFileTree = (o, p = '') =>
Object .entries (o) .map (([filename, type], _, __, path = p ? p '.' filename : filename) =>
Object (type) === type
? {filename, path, children: buildFileTree (type, path)}
: {filename, path, type}
)
The _
and __
are simply placeholder variables for those parameters. It's not horrible, but it's still a bit ugly. I will often choose this because it's syntactically a bit simpler than my other option. That one adds an extra IIFE as a place to hold a parameter for my additional value. It would look like this:
const buildFileTree = (o, p = '') =>
Object .entries (o) .map (([filename, type]) => ((
path = p ? p '.' filename : filename
) => Object (type) === type
? {filename, path, children: buildFileTree (type, path)}
: {filename, path, type}
) ())
const treeData = {cars: [{model: "", make: "Audi", year: ""}, {model: "A8", make: "", year: "2007"}], toys: {color: "Black", type: [{brand: "", price: "$100"}]}, id: "xyz", books: [{publisher: [{authors: []}]}], extra: "test"}
console .log (buildFileTree (treeData))
.as-console-wrapper {max-height: 100% !important; top: 0}
<iframe name="sif1" sandbox="allow-forms allow-modals allow-scripts" frameborder="0"></iframe>
Any of these versions should work. They all ignore your new FileNode()
call; I don't know what it is or how necessary it is, but it wouldn't be hard to integrate it.
Also, I don't know how you plan on using these path
s, but for such cases, I usually prefer to store them not as .
-separate strings, but as arrays of strings and integers (for array indices). That wouldn't be much different, and I think it gives more flexibility. And you can always join them back to a string with .join ('.')
. We could do it like this:
const buildFileTree = (o, p = []) =>
Object .entries (o) .map (([filename, type]) => ((
path = [...p, Array .isArray (o) ? Number(filename) : filename]
) => Object (type) === type
? {filename, path, children: buildFileTree (type, path)}
: {filename, path, type}
) ())
1 I like to do this Ramda-style (disclaimer: I'm one of it's authors) and build
isObject
atop a genericis
:const is = (Ctor) => (val) => val != null && val.constructor === Ctor || val instanceof Ctor const isObject = is (Object)