1

In my home project I try to show data in tree hierarchy. I send get call to API and I received this json:

{"items":[{"id":"1","name":"Adam","coWorkerId": 22 },{"id":"2","name":"Cris","age":32, "coWorkerId": 33}]}

And I would like to to display something like a tree below:

enter image description here

Then when I click for example Adam I need to send api request with id 22 because "coWorkerId": 22 could have also more coWorkerIds: for example:

{"items":[{"id":"22","name":"Jean","coWorkerId": 44 },{"id":"12","name":"Noah","age":32, "coWorkerId": 55}]}

enter image description here

I want to load this data on demand to tree hierarchy. Every click shoud generate I think api call. Of course Everey another call might not be the las because my node could hahe more child nodes.

So in html I tried:

<ng-container *ngTemplateOutlet="treeViewList; 
                                 context:{$implicit:users}">
</ng-container>

<ng-template #treeViewList let-list>
    <ul>
        <li *ngFor="let user of list; let i = index">
            <button (click)="onClick(coWorkerId.id)"> {{user.name}}</button>
        </li>
    </ul>
</ng-template>

data.service.ts


@Injectable({
  providedIn: 'root'
})
export class DataService {

  constructor(private httpClient: HttpClient) { }

  public getUsers(): Observable<UserResult> {
    return this.httpClient.get<UserResult>(baseUrl);
  }

    public getUsers(id): Observable<UserResult>{
    return this.httpClient.get<UserResult>(leafUsersUrl + id);
  }
}

component.ts

export class SidebarComponent implements OnInit {
  users: Users[];
  coWorkerId: String;

  constructor(private dataService: DataService) { }

  ngOnInit(): void {
     this.getRootUsers();
  }

  onClick(coWorkerId: String) {
    this.coWorkerId = coWorkerId;
    this.getLeafUsers(this.coWorkerId);
  }

  private getRootUsers() {
    this.dataService.getRootUsers().subscribe(
      data => {
        this.users = data.items;
      }
    )
  }

  private getLeafUsers(id: String) {
    this.dataService.getUsers(id).subscribe(
      data => {
        this.users = data.items;
      }
    );
  }
}

Instead of hierarchy I got list

-Adam
-Cris 

and when I click on Adam the whole list is changing to:

-Jean
-Noah

instead of create hierarchy. I was thinking about using import {MatTreeModule} from '@angular/material/tree';. Any ideas how to get this tree done?

R. Richards
  • 24,603
  • 10
  • 64
  • 64
derirative23
  • 486
  • 2
  • 7
  • 22

1 Answers1

0

This is because you are overwriting your list of users rather than adding new ones. A very simple solution would be to add a "children" field to the user and then do something like

<ul>
  <li *ngFor="let user of list">
      <button (click)="onClick(user)"> {{user.name}}</button>
      <ul *ngIf="user.children != null">
          <li *ngFor="let child of user.children">{{child.name}}</li>
      </ul>
  </li>
</ul>

Where onClick adds the loaded users to the children field

onClick(user) {
    this.dataService.getUsers(user.id).subscribe(x => user.children = x);
}

If you also add an "expanded" field and then expand / collapse the child nodes based on that, you have a simple functional tree.

This is a very simple solution, but not ideal. Your idea with using mat-tree is good, but it is surprisingly complicated.

You would need to define your tree node

export class StackOverflowNode {
  constructor(public item: User, public level = 1, public expandable = false,
              public isLoading = false) {}
}

define your data source

export class StackOverflowDataSource implements DataSource<StackOverflowNode> {
  dataChange = new BehaviorSubject<StackOverflowNode[]>([]);

  get data(): StackOverflowNode[] { return this.dataChange.value; }
  set data(value: StackOverflowNode[]) {
    this._treeControl.dataNodes = value;
    this.dataChange.next(value);
  }

 constructor(private _treeControl: FlatTreeControl<StackOverflowNode>,
              private _service: UserService) {}

 connect(collectionViewer: CollectionViewer): Observable<StackOverflowNode[]> {
    this._treeControl.expansionModel.changed.subscribe(change => {
      if ((change as SelectionChange<StackOverflowNode>).added ||
        (change as SelectionChange<StackOverflowNode>).removed) {
        this.handleTreeControl(change as SelectionChange<StackOverflowNode>);
      }
    });

    return merge(collectionViewer.viewChange, this.dataChange).pipe(map(() => this.data));
  }

  disconnect(collectionViewer: CollectionViewer): void {}

  /** Handle expand/collapse behaviors */
  handleTreeControl(change: SelectionChange<StackOverflowNode>) {
    if (change.added) {
      change.added.forEach(node => this.toggleNode(node, true));
    }
    if (change.removed) {
      change.removed.slice().reverse().forEach(node => this.toggleNode(node, false));
    }
  }

  /**
   * Toggle the node, remove from display list
   */
  toggleNode(node: StackOverflowNode, expand: boolean) {
    node.isLoading = true;

    this._service.getUsers(node.item.id).subscribe(children => {
      const index = this.data.indexOf(node);
      if (!children || index < 0) { // If no children, or cannot find the node, no op
        return;
      }

      if (expand) {
        const nodes = children.map(child =>
          new StackOverflowNode(child, node.level + 1, false));
        this.data.splice(index + 1, 0, ...nodes);
      } else {
        let count = 0;
        for (let i = index + 1; i < this.data.length
          && this.data[i].level > node.level; i++, count++) {}
         this.data.splice(index + 1, count);
      }

      // notify the change
      this.dataChange.next(this.data);
      node.isLoading = false;      
    });

  }

And use that in your component

@Component({
  selector: 'app-sidebar',
  templateUrl: './sidebar.component.html',
  styleUrls: ['./sidebar.component.scss']
})
export class SidebarComponent {

  treeControl: FlatTreeControl<StackOverflowNode>;

  dataSource: StackOverflowDataSource;

  getLevel = (node: StackOverflowNode) => node.level;

  isExpandable = (node: StackOverflowNode) => node.expandable;

  hasChild = (_: number, _nodeData: StackOverflowNode) => _nodeData.expandable

  constructor(private dataService: DataService) { 
    this.treeControl = new FlatTreeControl<StackOverflowNode>(this.getLevel, this.isExpandable);
    this.dataSource = new StackOverflowDataSource(this.treeControl, this.userService);

    this.dataService.getUsers().subscribe(x => this.dataSource.data  = x.map(data => new StackOverflowNode(data,0,true,false)))

  }


}

And use it in your html

<mat-tree [dataSource]="dataSource" [treeControl]="treeControl">
    <mat-tree-node *matTreeNodeDef="let node" matTreeNodePadding>
      <button mat-icon-button disabled></button>
      {{node.item.name}}
    </mat-tree-node>
    <mat-tree-node *matTreeNodeDef="let node; when: hasChild" matTreeNodePadding>
      <button mat-icon-button
              [attr.aria-label]="'toggle ' + node.filename" matTreeNodeToggle>
        <mat-icon class="mat-icon-rtl-mirror">
          {{treeControl.isExpanded(node) ? 'expand_more' : 'chevron_right'}}
        </mat-icon>
      </button>
      {{node.item.name}}
      <mat-progress-bar *ngIf="node.isLoading"
                        mode="indeterminate"
                        class="example-tree-progress-bar"></mat-progress-bar>
    </mat-tree-node>
  </mat-tree>

This has been adapted for your data sources from https://material.angular.io/components/tree/examples. You can study that page to see how this code works. Note that in order to keep it simple, I have made the assumption that your list only has 2 levels - instead of actually checking if a node has children, I automatically assume that level 0 nodes do nad level 1 nodes don't.

Jakub Judas
  • 737
  • 6
  • 16
  • I try to implement your solution but in `this.dataService.getUsers().subscribe(x => this.dataSource.data = x.map(data => new StackOverflowNode(data,0,true,false)))` I got an error `Property 'map' does not exist on type 'User` And i can not handle this... – derirative23 Jun 22 '20 at 09:08
  • That looks like your this.dataService.getUsers() is now returning a single user instead of a list. Can you check that? – Jakub Judas Jun 22 '20 at 09:18
  • So i changed dataService.getUsers() to return list, And underscor in code is gone. But in console now I got TypeError: x.map is not a function and I stucked again... FYI I Imported `import { map } from 'rxjs/operators';` and I got 'map' is declared but its value is never read – derirative23 Jun 22 '20 at 10:05
  • It's probably still not an array - an array always has .map. You can check the type of the variable in the browser's debugger. – Jakub Judas Jun 22 '20 at 10:09
  • As i check it is an array. I do the api call in: `public getRootUsers(): Observable { return this.httpClient.get(baseUrl); }` and `this.dataService.getRootUsers().subscribe(x => this.dataSource.data = x.map(data => new StackOverflowNode(data, 0, true, false))) ` Or am I doing something wrong? – derirative23 Jun 22 '20 at 10:56
  • What's the output of "this.dataService.getRootUsers().subscribe(x => console.log(x)))" ? – Jakub Judas Jun 22 '20 at 12:01
  • You were right. That wasn't an array. I changed code to: this.dataService.getRootOrganizations(1).subscribe(x => { this.dataSource.data = x.items.map(data => new StackOverflowNode(data, 0, true, false))}) } I need to debug data-source.ts becasue html is displaying list of [Object object]. May I ask you more questions in case of any problems? – derirative23 Jun 22 '20 at 15:54
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/216454/discussion-between-jakub-judas-and-derirative23). – Jakub Judas Jun 22 '20 at 19:21