Создавайте древовидные представления по запросу - PullRequest
1 голос
/ 20 июня 2020

В моем домашнем проекте я стараюсь отображать данные в виде древовидной иерархии. Я отправил вызов API и получил это json:

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

И я хотел бы отобразить что-то вроде дерева ниже:

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:

 {{user.name}}    

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;
      }
    );
  }
}

Вместо иерархии я получил список

-Adam
-Cris 

и когда я нажимаю на Адама, весь список меняется на:

-Jean
-Noah

вместо создания иерархии. Я думал об использовании import {MatTreeModule} from '@angular/material/tree';. Есть идеи, как сделать это дерево?

1 Ответ

0 голосов
/ 21 июня 2020

Это потому, что вы перезаписываете свой список пользователей, а не добавляете новых. Очень простым решением было бы добавить пользователю «дочернее» поле, а затем выполнить что-то вроде

<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>

Где onClick добавляет загруженных пользователей в дочернее поле

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

If вы также добавляете «расширенное» поле, а затем разворачиваете / свертываете дочерние узлы на основе этого, у вас есть простое функциональное дерево.

Это очень простое решение, но не идеальное. Ваша идея с использованием дерева матов хороша, но на удивление сложна.

Вам нужно будет определить узел вашего дерева

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

определить источник данных

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;      
    });

  }

И используйте это в своем компоненте

@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)))

  }


}

И используйте его в своем 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>

Это было адаптировано для ваших источников данных из https://material.angular.io/components/tree/examples. Вы можете изучить эту страницу, чтобы увидеть, как работает этот код. Обратите внимание, что для простоты я сделал предположение, что ваш список имеет только 2 уровня - вместо того, чтобы фактически проверять, есть ли у узла дочерние элементы, я автоматически предполагаю, что узлы уровня 0 имеют, а узлы уровня 1 - нет.

...