Home > Software engineering >  How to store the path while building an angular material tree
How to store the path while building an angular material tree

Time:10-23

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 paths, 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 generic is:

const is = (Ctor) => (val) => 
  val != null && val.constructor === Ctor || val instanceof Ctor
const isObject = is (Object)
  • Related