0
votes

I am trying to build a hierarchical expand/collapse list to represent a parent child relationship. Upon Initial load, the parent nodes will be listed. If they have children, a carat icon is displayed otherwise, a bullet icon is displayed. Upon clicking the carat icon, an API call is made to retrieve data and the child information is displayed. Upon clicking the same carat icon again, no API call is made, but the immediate child list is hidden. This should happen recursively in case the children are parent nodes.

Desired behavior on Initial load:

enter image description here

Desired behavior after carat icon is clicked to expand:

enter image description here

JSON Data

{
    "page": {
        "results": [{
            "id": "1001",
            "title": "American",
            "children": {
                "page": {
                    "results": [{
                        "id": "1003",
                        "title": "Chevy",
                        "children": {
                            "page": {
                                "results": [],
                                "start": 0,
                                "limit": 25,
                                "size": 0
                            }
                        }
                    }],
                    "start": 0,
                    "limit": 25,
                    "size": 1
                }
            }
        }, {
            "id": "1002",
            "title": "German",
            "children": {
                "page": {
                    "results": [],
                    "start": 0,
                    "limit": 25,
                    "size": 0
                }
            }
        }],
        "start": 0,
        "limit": 2,
        "size": 2
    }
}

Component Code

import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/throw';

import { WikiService } from '../wiki.service';
import { WikiTree } from '../../interfaces/WikiTree';
import { AppConfigService } from '../../services/app-config.service';

@Component({
  selector: 'app-wiki-tree-nav',
  templateUrl: './wiki-tree-nav.component.html',
  styleUrls: ['./wiki-tree-nav.component.css']
})
export class WikiTreeNavComponent implements OnInit {

  public wikiPageId: any = '1000';
  wikiTree$: Observable<WikiTree>;
  public resultsPage: any;

  constructor(private wikiService: WikiService, private route: ActivatedRoute,
    private appConfigService: AppConfigService) {
    this.route.params.subscribe(res => this.wikiPageId = res.id);
  }

  ngOnInit() {
      this.getWikiTree(0, 200, this.wikiPageId);
  }

  getWikiTree(start: number = 0, limit: number = 200, pageId: string = '') {
    this.wikiTree$ = this.wikiService.GetWikiTree(start, limit, pageId);
    this.wikiTree$.subscribe((data) => {
      this.resultsPage = data;
    },
      (err) => {
      }
    );
  }

  getCSSClass(pageId: string) {
    let cssClass = '';
    if (this.wikiPageId === pageId) {
      cssClass = 'boldTitle';
    }
    return cssClass;
  }

}

HTML Code

<ul *ngIf="wikiTree$ | async as wikiTree">
        <li *ngFor="let result of wikiTree.page.results; index as i" style="margin-bottom:10px;">
            <div>
                <span *ngIf="result.children.page.size==0" style="margin-left:4px;margin-right:4px;">
                    <clr-icon shape="circle" class="is-solid" size="6"></clr-icon>
                </span>
                <span *ngIf="result.children.page.size>0">
                    <a (click)="getWikiTree(0,200,result.id)">
                        <clr-icon shape="caret right" size="14"></clr-icon>
                    </a>
                </span>
                <span style="margin-left:5px;font-size:14px;" [ngClass]="getCSSClass(result?.id)">
                    <a href="/pages/{{result.id}}">{{result.title}}</a>
                </span>
            </div>
        </li>
    </ul>

My current HTML code replaces the entire tree instead of appending to the parent nodes. How can I implement the Angular HTML code to perform the expansion and collapse recursively?

1

1 Answers

0
votes

I go to create a structural directive, so my app.component.html becomes like

<div recursive [children]="data" [search]="dataService.getData"></div>

where I defined

  constructor(public dataService:DataService){}
  ngOnInit()
  {
    this.dataService.getData(0).subscribe(res=>this.data=res)
  }

I need pass the "search" function to the recursive component that return an Observable of an array of objects, the objects has as properties 'id','label' and 'hasChildren' to indicate if has children or not, e.g. the json response becomes like

[{id:1,label:'label1',hasChildren:true},{id:2,label:'label2'}]

In a recursive component I like defined as @Input() level,children and parent. As you want to, in a click call to a function that get the "children", I add the search and in a "open" function, I change the property "isOpen" and call to the searchFunction. See that in the object that manage the component, at first there're no property 'children' nor 'isOpen', I add these "on-fly"

@Component({
  selector: "[recursive]",
  templateUrl: "./tree-view.component.html",
  styleUrls: ["./tree-view.component.css"]
})
export class TreeViewComponent {
  @Input() level: number;
  @Input() children: any;
  @Input() parent: any;

  @Input() search: (any) => Observable<any>;

  self = this;
  open(item) {
    item.isOpen = !item.isOpen;
    if (!item.children) {
      item.loading="...."
      this.search(item.id).subscribe(res=>{
        item.loading=null
        item.children=res;
      })
    }
  }
}

With this conditions, our tree-view.html is like

<ul class="tree" *ngIf="level==undefined">
  <ng-container *ngTemplateOutlet="tree;context:{children:children,search:search}">
  </ng-container>
</ul>
<ng-container *ngIf="level!=undefined">
  <ng-container *ngTemplateOutlet="tree;context:{children:children,search:search}">
  </ng-container>
</ng-container>

<ng-template #tree let-children="children" let-search="search">
<li *ngFor="let item of children">
    <div (click)="item.hasChildren && open(item)">
     <span [ngClass]="!item.hasChildren?'doc':item.isOpen?'open':'close'" ></span>
     {{item.label}}{{item.loading}}
  </div>
    <ul recursive *ngIf="item.children && item.isOpen" 
        [children]="item.children" 
        [parent]="self" 
        [level]="level!=undefined?level+1:0"
        [search]="search"
        >
  </ul>
</li>
</ng-template>

see stackblitz, I hope this help you

NOTE: I use three css class, .doc,.open and .close to indicate the states of the tree and a "fool" service with of and delay to simulate a call