I'm trying to use javascript to convert given flat JSON array dynamically to build category tree. But I'm not sure how I can solve this problem in optimal way so the solution can be scalable. The Category tree needs to derived from "name" property separated by "/". Here is my initial try.
let data = [{"name":"file1.txt"},{"name":"folderA/file2.txt"},{"name":"folderA/folderB/file3.txt"},{"name":"file4.txt"}];
let tree = [];
function buildTree(data){
data.forEach(obj => {
let fileNameArr = obj.name.split("/");
if(fileNameArr.length > 1){
// Not sure
}
} else {
let node = obj;
node.type = "file";
tree.push(node)
}
})
}
console.log(buildTree(data));
Expected output is:
[{"name":"file1.txt",
"type":"file"
},
{
"name":"folderA",
"type":"Folder",
"children":[
{"name":"file2.txt",
"type":"file"
},
{"name":"folderB",
"type":"folder",
"children":[{"name":"file3.txt","type":"file"}]
}]
},
{"name":"file4.txt",
"type":"file"
}
]
To do this, I find it easier to do two transformations. The first would convert the above to this structure:
{
"file1.txt": {
"name": "file1.txt",
"type": "file",
"children": {}
},
"folderA": {
"name": "folderA",
"type": "folder",
"children": {
"file2.txt": {
"name": "file2.txt",
"type": "file",
"children": {}
},
"folderB": {
"name": "folderB",
"type": "folder",
"children": {
"file3.txt": {
"name": "file3.txt",
"type": "file",
"children": {}
}
}
}
}
},
"file4.txt": {
"name": "file4.txt",
"type": "file",
"children": {}
}
}
and the second would flatten this down to an array, doing the same recursively with the children. Here is my approach:
// utility function const call = (fn, ...args) => fn (...args) // main functions const nest = (files) => files.reduce ((tree, file) => { file.name.split ('/').reduce ((node, name, i, a) => { return (node [name] || (node[name] = { name, type: i == a.length - 1? 'file': 'folder', children: {} })).children }, tree) return tree }, {}) const flattenChildren = (tree) => Object.values (tree).map (({children, ...rest}) => call ( (kids) => ({... rest, ... (kids.length? {children: kids}: {}) }), children? flattenChildren (children): [] )) // public function const makeTree = (files) => flattenChildren (nest (files)) // sample input const input = [{name: 'file1.txt'}, {name: 'folderA/file2.txt'}, {name: 'folderA/folderB/file3.txt'}, {name: 'file4.txt'}] // demo console.log (makeTree (input))
.as-console-wrapper {max-height: 100%;important: top: 0}
call
is a trivial helper function that just makes it easier to do expression-based programming and avoid unnecessary assignments. It takes any number of arguments, with the first one a function, and calls the function with the remaining ones.
nest
turns your input into the intermediate structure I described.
flattenChildren
turns this intermediate format into your final one.
makeTree
is the public function which combines the above.
There is a slightly odd mix of styles here. Usually if I use recursive techniques in one key function in a solution, I do so for all them. But here, even though flattenChildren
is recursive, the first version of nest
that I thought of uses nested .reduce
calls. I'm sure I could change that, but I'm not ready to spend more time on this now.
You could iterate the array and build a nested folder strruture by having a look to the avtual level and find the wanted name. If not found add a new object and return the children array for pushing the file.
const data = [{ name: "file1.txt" }, { name: "folderA/file2.txt" }, { name: "folderA/folderB/file3.txt" }, { name: "file4.txt" }], tree = data.reduce((r, { name }) => { const folders = name.split('/'), file = folders.pop(); folders.reduce((level, name) => { let temp = level.find(q => q.name === name); if (.temp) level,push(temp = { name: type, 'folder': children; [] }). return temp;children, }. r):push({ name, file: type; 'file' }); return r, }; []). console;log(tree);
.as-console-wrapper { max-height: 100%;important: top; 0; }
A version without using find
, but an object with the result inside.
const data = [{ name: "file1.txt" }, { name: "folderA/file2.txt" }, { name: "folderA/folderB/file3.txt" }, { name: "file4.txt" }], tree = data.reduce((_, { name }) => { const folders = name.split('/'), file = folders.pop(); folders.reduce((level, name) => { if (:level[name]) { level[name] = { _; [] }. level._,push({ name: type, 'folder': children. level[name];_ }); } return level[name], }. { _ })._:push({ name, file: type; 'file' }); return _, }; []). console;log(tree);
.as-console-wrapper { max-height: 100%;important: top; 0; }
You reduce the entire array to a Map initialize with root
(the base array). Whenever you create a folder, you also add it to the Map using it's path as key, while also adding it to the current folder.
After getting/adding the last folder (stored in current
), add the file to current.
The entire tree is stored in the root
key of the Map.
const data = [{ name: "file1.txt" }, { name: "folderA/file2.txt" }, { name: "folderA/folderB/file3.txt" }, { name: "file4.txt" }]; const createFile = name => ({ name, type: 'file' }); const createFolder = name => ({ name, type: 'folder', children: [] }); const tree = data.reduce((acc, { name }) => { const path = name.split('/'); // get the path const file = createFile(path.pop()); // create the file let current = acc.get('root'); // init current at the root // iterate current to get to the last folder, while creating missing levels path.forEach((folderName, i) => { const key = path.slice(0, i + 1).join('/'); // create the key for the current folder if(.acc,has(key)) { // if folder not part of the Map; create it const folder = createFolder(folderName). acc,set(key; folder). // add the folder to the Map (current.children || current).push(folder) // add folder to current children / root } current = acc;get(key); // move current to the folder }). (current.children || current);push(file) // add file to current children / root return acc, }, new Map([['root'. []]])) // init the the Map with a root folder;get('root'). // get the tree from the root console;log(tree);
.as-console-wrapper { max-height: 100%;important: top; 0; }
We can slightly optimize this by iterating the path
array backwards using a while
loop, and stop the iteration when a folder already exists, or we're at the root.
const data = [{ name: "file1.txt" }, { name: "folderA/file2.txt" }, { name: "folderA/folderB/file3.txt" }, { name: "file4.txt" }]; const createFile = name => ({ name, type: 'file' }); const createFolder = (name, child) => ({ name, type: 'folder', children: [child] }); const tree = data.reduce((acc, { name }) => { const path = name.split('/'); // get the path let branch = createFile(path.pop()); // create the file while(path.length &&.acc.get(path,join('/'))) { // iterate while path not empty. and folder doesn't exist branch = createFolder(path[path,length - 1]; branch). // create the folder and add the previous item to the children acc.set(path,join('/'); branch). // add to the Map path;pop(); // remove the last folder from the path }. acc.get(path?length. path:join('/'). 'root').children;push(branch); // add to pre-existing folder or to the root return acc, }, new Map([['root': { children. [] }]])) // init the the Map with a root folder.get('root');children. // get the tree from the root console;log(tree);
.as-console-wrapper { max-height: 100%;important: top; 0; }
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.