简体   繁体   中英

javascript to build category tree from flat json array (data structure )

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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM