3

I am currently in the process of trying to build out a hierarchy view of users. My end goal is to generate the view using this hierarchy view or something similar.

The difficulty lies in how the JSON objects used to generate the hierarchy are given. This is a sample response (this response can be much bigger), where pid is the parent id and depth is the distance from the first parent.

response = [
    {uid: "abc", pid: null, depth: 1, parent: true},
    {uid: "def", pid: "abc", depth: 2, parent: false},
    {uid: "ghi", pid: "abc", depth: 2, parent: true},
    {uid: "jkl", pid: "ghi", depth: 3, parent: false},
    {uid: "mno", pid: "ghi", depth: 3, parent: false},
]

To explain the above response better, here is the visual hierarchy view of it: image

A lot of the answers and solutions I've seen so far utilize JSON with children nested in each one. Is it possible to generate the view using the json model above?

Any help or insight would be much appreciated! Thanks!

georgeawg
  • 48,608
  • 13
  • 72
  • 95
Aido
  • 150
  • 4
  • 9

2 Answers2

7

First, you need to convert your self-reference table to a hierarchical table (tree). I suggest you use a custom pipe to do this since you will be able to reuse this pipe in other places.

You can use Reactgular's code, my code from the StackOverflow thread, or write your own code. I create my converter pipe with Reactgular's code:

converter.pipe.ts

import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name: 'converter'
})
export class ConverterPipe implements PipeTransform {
  transform(array: any[], id: string = 'uid', parentId: string = 'pid'): any[] {
    const map = array.reduce(
      (acc, node) => ((node.items = []), (acc[node[id]] = node), acc),
      {}
    );

    return Object.values(map)
      .map(
        node => (node[parentId] && map[node[parentId]].items.push(node), node)
      )
      .filter(node => node[parentId] === null);
  }
}

Don't forget to add it to the declaration section of your module:

app.module.ts

import { ConverterPipe } from './converter.pipe';

@NgModule({
  declarations: [
    ConverterPipe
  ]
})
export class AppModule { }

Now, you can create your component template and use the approach from the Hierarchy View CodePen. Since you need different markup for branches and leaves, it's handy to use NgTemplateOutlet's and NgIf structural directives. This's a good idea to move the level markup in a template and reuse it when you need to render a tree in Angular. The same idea is illustrated in my answer. Based on the provided CodePen code, your Angular markup may look as the following:

app.component.html

<div class="hv-wrapper">
  <ng-template #Item let-item>
    <ng-container *ngIf="!item.items.length; else Component">
      <p>{{ item.uid }}</p>
    </ng-container>
    <ng-template #Component>
      <div class="hv-item">
        <div class="hv-item-parent">
          <p>{{ item.uid }}</p>
        </div>
        <div class="hv-item-children">
          <div class="hv-item-child" *ngFor="let child of item.items">
            <ng-container
              *ngTemplateOutlet="Item; context: { $implicit: child }"
            ></ng-container>
          </div>
        </div>
      </div>
    </ng-template>
  </ng-template>

  <ng-container *ngFor="let child of response | converter"
    ><ng-container
      *ngTemplateOutlet="Item; context: { $implicit: child }"
    ></ng-container
  ></ng-container>
</div>

Here, response is your original array:

app.component.ts

export class AppComponent {
  response = [
    { uid: 'abc', pid: null, depth: 1, parent: true },
    { uid: 'def', pid: 'abc', depth: 2, parent: true },
    { uid: 'ghi', pid: 'abc', depth: 2, parent: false },
    { uid: 'jkl', pid: 'ghi', depth: 3, parent: false },
    { uid: 'mno', pid: 'ghi', depth: 3, parent: false }
  ];
}

Don't forget to use the CodePen SASS styles in your project.

After this, you will a graph like:

graph

This is a StackBlitz project that demonstrates this approach in action.

Gosha_Fighten
  • 3,838
  • 1
  • 20
  • 31
3

You can use a reducer to convert the flat array into UID map of nodes, and once you have the map you can populate the children easily. You can then just pick out the root node and use that to render the HTML.

const map = [
   {uid: "abc", pid: null, depth: 1, parent: true},
   {uid: "def", pid: "abc", depth: 2, parent: true},
   {uid: "ghi", pid: "abc", depth: 2, parent: false},
   {uid: "jkl", pid: "ghi", depth: 3, parent: false},
   {uid: "mno", pid: "ghi", depth: 3, parent: false},
].reduce((acc, node) => (node.children = [], acc[node.uid] = node, acc), {});

const [root] =  Object.values(map)
                      .map(node => (node.pid && map[node.pid].children.push(node), node))
                      .filter(node => node.pid === null);
    
console.log(root);

You can render the tree by using the same component recursively, and have the template render the children.

@Component({
      selector: 'app-node',
      template: `
          <span>Node</span>
          <app-node [node]="child" *ngFor="let child of node.children"></app-node>
      `
})
export class NodeComponent {
    @Input()
    public node: any;
}

It's not hard to modify the above to match the HTML/CSS you linked to in your question.

Reactgular
  • 52,335
  • 19
  • 158
  • 208