Tree view constancy being nested after every refresh

本秂侑毒 提交于 2021-01-28 06:01:55

问题


I am new to Angular (7) coming from a c# background. I am using asp.net core 2.2 and using the default template which comes with angular new angular project( home, counter, fetch-data). The tree view is bound and is coming from a controller.

I am expecting

> Europe
>>  England
>>>   Manchester
>>>   London
>>>>    London City
>>>>    Stratford
>>  Germany

however, i'm getting, every time i expand

>   Europe
>>    Europe
>>>      Europe
>>>>        Europe

and so on

my code (home page which shows as soon as ISS express is loaded)

<p>
Testing Componenets
<app-tree-view> </app-tree-view>

tree-view.component.html

<ul>
  <li *ngFor="let n of nodes">
    <span><input type="checkbox" [checked]="n.checked" (click)="n.toggle()" /></span>
    {{ n.name }}>
    <div *ngIf="n.expanded">
      <app-tree-view [nodes]="n.nodes"></app-tree-view>
    </div>
  </li>
</ul>

tree-view.component.ts

import { Component, Inject, Input, OnInit } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Component({
  selector: 'app-tree-view',
  templateUrl: './tree-view.component.html'
})
export class TreeViewComponent {

  @Input() nodes: Array<Node>;

  constructor(http: HttpClient, @Inject('BASE_URL') baseUrl: string) {
    this.nodes = [];
    http.get<Node[]>(baseUrl + 'api/FunctionNodes/GetNodes').subscribe(result => {
      this.nodes = this.RecursiveMapNodes(result.map(x => new Node(x.id, x.name, x.nodes)));
    }, error => console.error(error));
  }

  RecursiveMapNodes(nodes: Array<Node>): Array<Node> {
    var result = Array<Node>();
    for (let node of nodes) {
      var n = new Node(node.id, node.name, this.RecursiveMapNodes(node.nodes));
      result.push(n);
    }
    return result;
  }
}

export class Node {
  id: number;
  name: string;
  nodes: Array<Node>;
  checked: boolean;
  expanded: boolean;

  constructor(id: number, name: string, nodes: Array<Node>) {
    this.id = id;
    this.name = name;
    this.nodes = nodes;
    this.checked = false;
    this.expanded = false;
  }

  toggle() {
    this.expanded = !this.expanded;
  }
  check() {
    let newState = !this.checked;
    this.checked = newState;
    this.checkRecursive(newState);
  }

  checkRecursive(state) {
    this.nodes.forEach(d => {
      d.checked = state;
      d.checkRecursive(state);
    })
  }

}

FunctionNodesController.cs

 [Route("api/[controller]")]
    public class FunctionNodesController : Controller
    {
        [HttpGet("[action]")]
        public IEnumerable<Node> GetNodes()
        {
            var node_1 = new Node() { Id = 1, Name = "Europe" };
            var node_1_1 = new Node() { Id = 2, Name = "England" };
            var node_1_1_1 = new Node() { Id = 3, Name = "Manchester" };
            var node_1_1_2 = new Node() { Id = 4, Name = "London" };
            var node_2_1_1 = new Node() { Id = 5, Name = "London City" };
            var node_2_1_2 = new Node() { Id = 6, Name = "Stratford" };
            var node_1_2 = new Node() { Id = 7, Name = "Germany" };

            node_1.Nodes.Add(node_1_1);
            node_1_1.Nodes.Add(node_1_1_1);
            node_1_1.Nodes.Add(node_1_1_2);
            node_1_1_2.Nodes.Add(node_2_1_1);
            node_1_1_2.Nodes.Add(node_2_1_2);
            node_1.Nodes.Add(node_1_2);
            return new List<Node>() { node_1 };
        }

        public class Node
        {
            public int Id { get; set; }
            public string Name { get; set; }
            public List<Node> Nodes { get; set; }
            public Node()
            {
                Nodes = new List<Node>();
            }
        }
    }

app.module.tss

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpClientModule } from '@angular/common/http';
import { RouterModule } from '@angular/router';

import { AppComponent } from './app.component';
import { NavMenuComponent } from './nav-menu/nav-menu.component';
import { HomeComponent } from './home/home.component';
import { CounterComponent } from './counter/counter.component';
import { FetchDataComponent } from './fetch-data/fetch-data.component';
import { MainWindowComponent } from './main-window/main-window.component';
import { TreeViewComponent } from './tree-view/tree-view.component';

@NgModule({
  declarations: [
    AppComponent,
    NavMenuComponent,
    HomeComponent,
    CounterComponent,
    FetchDataComponent,
    MainWindowComponent,
    TreeViewComponent
  ],
  imports: [
    BrowserModule.withServerTransition({ appId: 'ng-cli-universal' }),
    HttpClientModule,
    RouterModule.forRoot([
      { path: '', component: MainWindowComponent, pathMatch: 'full' },
      { path: 'counter', component: CounterComponent },
      { path: 'fetch-data', component: FetchDataComponent },
    ])
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

回答1:


Hello I have created a little demo that you can use as reference, for the problem that you are tackling, you can find it here at CodeSandbox.

As im not very knowledgeable about the C# part, I have created a mock back-end service that is supposed to act as your back-end.

About the question, why does it not work, as you had already mentioned in the comments, every time you are initializing (going a level in) your tree-view.component.ts inside of it's constructor you are fetching the data, which results in always getting 'Europe' displayed as result.

When creating recursive elements (trees and etc.) you must always provide to the recursive component (in your case tree-view.component.ts) the next layer of the tree.

For example first 'Europe' => ['England' => ['Manchester' , 'London' => ['London city', 'Stratford] ] , 'Germany'] , where each => is building new tree-view.component.ts

// Template
<div [ngStyle]="{'margin-left':level*12 +'px'}" *ngFor="let node of nodes">
  <h1>Current node = {{node?.name}}</h1>
  <div
    (click)="open=!open"
    style="cursor: pointer;"
    *ngIf="node?.nodes?.length!==0"
  >
    Expand nodes
  </div>
  <div *ngIf="open">
    <app-tree [level]="level+1" [nodesForDisplay]="node.nodes"></app-tree>
  </div>
</div>
// Component
@Component({
  selector: "app-tree",
  templateUrl: "./tree.component.html"
})
export class TreeComponent implements OnInit {
  @Input("nodesForDisplay") nodes;
  @Input("level") level = 0;
  open = false;
}

So in order to escape this deadlock, of always fetching the first layer, you can try creating a wrapper parent component, that handles the fetching and passes the data to the recursive components.

Another tip, is to remove the http call from the component and place it in a dedicated service, that you will inject inside of the components where needed (as done in the example)

// Template
<app-tree [nodesForDisplay]="{{place to strore the data}}"></app-tree>

// Component
@Component({
  selector: "app-container",
  templateUrl: "./app.component.html",
  styleUrls: ["./app.component.css"]
})
export class ContainerComponent {
  ...
  constructor(public fetcher: {{Your Service}}) {}
  ngOnInit() {
  this.fetcher.{{method to fethc the data}}.subscribe(x =>{
  this.{{place to strore the data}} = x
  })
  }
}



回答2:


i'm posting the code of the question and how it changed from the first post (FunctionNodesController.cs remains the same as original post), i don't know the best practices when it comes to angular so if you see something bad please leave a comment. (FunctionNodesController for this case can be renamed to RegionNodesController and similarly for FunctionService to RegionService)

tree-view.component.ts

import { Component, Input,  } from '@angular/core';
import { TreeNode } from '../../classes/TreeNode';

@Component({
  selector: 'app-tree-view',
  templateUrl: './tree-view.component.html'
})
export class TreeViewComponent{

  @Input() nodes: Array<TreeNode>;
  constructor() {

  }
}

tree-view.component.html

<ul *ngIf="nodes">
  <li *ngFor="let n of nodes">
    <span><input type="checkbox" (click)="n.toggle()" /></span>
    {{ n.name }}
    <div *ngIf="n.expanded">
      <app-tree-view  [nodes]="n.nodes"></app-tree-view>
    </div>
  </li>
</ul>

main-window.html

<app-tree-view [nodes]="nodes"> </app-tree-view>

main-window.ts

import { Component, Inject, OnInit } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { TreeNode } from '../../classes/TreeNode';
import { FunctionService } from '../../services/functionService';

@Component({
  selector: 'app-main-window',
  templateUrl: './main-window.component.html',
})
export class MainWindowComponent{
  nodes = [];

  constructor(private funcService: FunctionService) {
    funcService.getNodeData().subscribe(result => {
      this.nodes = this.RecursiveMapNodes(result.map(x => new TreeNode(x.id, x.name, x.nodes)));
    }, error => console.error(error));

  }
    <!--this is required as during serialisation the methods/ function in nodes are not instanced unless we create a new object-->
  RecursiveMapNodes(nodes: Array<TreeNode>): Array<TreeNode> {
    var result = Array<TreeNode>();
    for (let node of nodes) {
      var n = new TreeNode(node.id, node.name, this.RecursiveMapNodes(node.nodes));
      result.push(n);
    }
    return result;
  }

}

TreeNode.ts (i renamed Node to treeNode)

export class TreeNode {
  id: number;
  name: string;
  nodes: Array<TreeNode>;
  checked: boolean;
  expanded: boolean;

  constructor(id: number, name: string, nodes: Array<TreeNode>) {
    this.id = id;
    this.name = name;
    this.nodes = nodes;
    this.checked = false;
    this.expanded = false;
  }

  toggle() {
    this.expanded = !this.expanded;
  }
  check() {
    let newState = !this.checked;
    this.checked = newState;
    this.checkRecursive(newState);
  }

  checkRecursive(state) {
    this.nodes.forEach(d => {
      d.checked = state;
      d.checkRecursive(state);
    })
  }
}

functionService.ts

import { Injectable, Inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { TreeNode } from '../classes/TreeNode';
import { Observable } from 'rxjs';

@Injectable()
export class FunctionService {

  constructor(private http: HttpClient, @Inject('BASE_URL') private baseUrl: string) { }

  getNodeData(): Observable<Array<TreeNode>> {
    return this.http.get<TreeNode[]>(this.baseUrl + 'api/FunctionNodes/GetNodes');
  }
}

app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpClientModule } from '@angular/common/http';
import { RouterModule } from '@angular/router';

import { AppComponent } from './components/app.component';
import { NavMenuComponent } from './components/nav-menu/nav-menu.component';
import { HomeComponent } from './components/home/home.component';
import { CounterComponent } from './components/counter/counter.component';
import { FetchDataComponent } from './components/fetch-data/fetch-data.component';
import { MainWindowComponent } from './components/main-window/main-window.component';
import { TreeViewComponent } from './components/tree-view/tree-view.component';
import { FunctionService } from './services/functionService';

@NgModule({
  declarations: [
    AppComponent,
    NavMenuComponent,
    HomeComponent,
    CounterComponent,
    FetchDataComponent,
    MainWindowComponent,
    TreeViewComponent
  ],
  imports: [
    BrowserModule.withServerTransition({ appId: 'ng-cli-universal' }),
    HttpClientModule,
    RouterModule.forRoot([
      { path: '', component: MainWindowComponent, pathMatch: 'full' },
      { path: 'counter', component: CounterComponent },
      { path: 'fetch-data', component: FetchDataComponent },
    ])
  ],
  providers: [FunctionService],
  bootstrap: [AppComponent]
})
export class AppModule { }


来源:https://stackoverflow.com/questions/56478434/tree-view-constancy-being-nested-after-every-refresh

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!