私はAngular 2でTreeViewを書いています

「Angular 2のエントリーしきい値-理論と実践」という記事に触発されて、私は創造性の苦痛についての記事を書くことも決めました。



ASP.NET WebFormsで書かれた大きなプロジェクトがあります。 そこには多くのものが混在していたので、だんだん好きにならなくなりました。 私はすべてを現代的なものに書き換えようとすることにしました。 私はすぐにAngular 2が好きで、試してみることにしました。 タスクは次のように定義されました。新しいフロントエンドを作成し、既存のバックエンドにねじ込み、後者への最小限の変更を行います。 新しいフロントエンドは、エンドユーザーが何にも気付かないように、古いフロントエンドとUI互換でなければなりません。



このようなスタックの合計:バックエンド-ASP.NET Web API、Entity Framework、MS SQL。 フロントエンド-Angular 2; ブートストラップ3テーマ。



TreeViewの結果をすぐに表示します。



画像



Visual StudioでAngular 2をセットアップするプロセスについては説明しません。これは広大な部分で完了しています。 追加する必要があるのは、ルート要求をindex.htmlにリダイレクトするweb.configの設定のみでした。



web.configの一部
<system.webServer> <modules runAllManagedModulesForAllRequests="true"/> <rewrite> <rules> <rule name="IndexRule" stopProcessing="true"> <match url=".*"/> <conditions logicalGrouping="MatchAll"> <add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true"/> <add input="{REQUEST_URI}" matchType="Pattern" pattern="^/api/" negate="true"/> </conditions> <action type="Rewrite" url="/index.html"/> </rule> </rules> </rewrite> </system.webServer>
      
      







すべてが正常に離陸しました。 静的ファイルは正しくロードされ、Web APIコントローラーはAPIを機能させ、他のルートは常にindex.htmlによって処理されます。



エンドポイントの記述を開始する前に、私は最初にWebFormのコントロールアナログをいくつか書くことにしました。 もちろん、ほとんどの場合、ListViewとFormViewが使用されます。 しかし、私はシンプルなTreeViewから始めることにしました。これはいくつかの形式でも必要です。



トラフィックを減らすため、必要なツリーノードのみをダウンロードすることにしました。 初期化するとき、トップレベルのみをリクエストします。



ノードが展開されると、子孫の存在を確認し、存在しない場合はonRequestNodesイベントを生成します。 ユーザーがノードを選択すると、onSelectedChangedイベントが生成されます。 Fontawesomeアイコン。



コンポーネントには2つの入力パラメーターがあります。Nodes-このレベルのノードのリスト、SelectedNode-ユーザーが選択したノード。 2つのイベント:onSelectedChanged-ユーザーが選択したノードの変更、onRequestNodes-必要に応じてノードの要求。 @Inputパラメーターは、親から子孫に伝搬します(階層に深く入ります)。 @Output()イベントは子孫から親(階層外)に伝播します。 コンポーネントは再帰的です-階層の新しいレベルはそれぞれ、コンポーネントの独自のインスタンスを処理します。



treeview.component.ts
 import {Component, Input, Output, EventEmitter} from 'angular2/core'; export interface ITreeNode { id: number; name: string; children: Array<ITreeNode>; } @Component({ selector: "tree-view", templateUrl: "/app/components/treeview/treeview.html", directives: [TreeViewComponent] }) export class TreeViewComponent { @Input() Nodes: Array<ITreeNode>; @Input() SelectedNode: ITreeNode; @Output() onSelectedChanged: EventEmitter<ITreeNode> = new EventEmitter(); @Output() onRequestNodes: EventEmitter<ITreeNode> = new EventEmitter(); constructor() { } onSelectNode(node: ITreeNode) { this.onSelectedChanged.emit(node); } onExpand(li: HTMLLIElement, node: ITreeNode) { if (this.isExpanden(li)) { li.classList.remove('expanded'); } else { li.classList.add('expanded'); if (node.children.length == 0) { this.onRequest(node); } } } onRequest(parent: ITreeNode) { this.onRequestNodes.emit(parent); } isExpanden(li: HTMLLIElement) { return li.classList.contains('expanded'); } }
      
      







treeview.html
 <ul class="treenodes"> <li #li *ngFor="#node of Nodes" class="treenode"> <i class="nodebutton fa" (click)="onExpand(li, node)" [ngClass]="{'fa-minus-square-o': isExpanden(li), 'fa-plus-square-o': !isExpanden(li)}"> </i> <span class="nodetext" [ngClass]="{'bg-info': node == SelectedNode}" (click)="onSelectNode(node)"> {{node.name}} </span> <tree-view [Nodes]="node.children" [SelectedNode]="SelectedNode" (onSelectedChanged)="onSelectNode($event)" (onRequestNodes)="onRequest($event)" *ngIf="isExpanden(li)"> </tree-view> </li> </ul>
      
      







スタイルは別のファイルを作成しました。



treeview.css
 tree-view .treenodes { list-style-type: none; padding-left: 0; } tree-view tree-view .treenodes { list-style-type: none; padding-left: 16px; } tree-view .nodebutton { cursor: pointer; } tree-view .nodetext { padding-left: 3px; padding-right: 3px; cursor: pointer; }
      
      







使用方法:



sandbox.component.ts
 import {Component, OnInit} from 'angular2/core'; import {NgClass} from 'angular2/common'; import {TreeViewComponent, ITreeNode} from '../treeview/treeview.component'; import {TreeService} from '../../services/tree.service'; @Component({ templateUrl: '/app/components/sandbox/sandbox.html', directives: [NgClass, TreeViewComponent] }) export class SandboxComponent implements OnInit { Nodes: Array<ITreeNode>; selectedNode: ITreeNode; //        . constructor(private treeService: TreeService) { } //      ngOnInit() { this.treeService.GetNodes(0).subscribe( res => this.Nodes = res, error => console.log(error) ); } //      onSelectNode(node: ITreeNode) { this.selectedNode = node; } //     onRequest(parent: ITreeNode) { this.treeService.GetNodes(parent.id).subscribe( res => parent.children = res, error=> console.log(error)); } }
      
      







sandbox.html
覚えておいて、私はブートストラップ3を持っています。



 <div class="col-lg-3"> <div class="panel panel-info"> <div class="panel-body"> <tree-view [Nodes]="Nodes" [SelectedNode]="selectedNode" (onSelectedChanged)="onSelectNode($event)" (onRequestNodes)="onRequest($event)"> </tree-view> </div> </div> </div>
      
      







tree.service.ts
最も原始的なサービス

 import {Injectable} from 'angular2/core'; import {Http} from 'angular2/http'; import 'rxjs/Rx'; @Injectable() export class TreeService { constructor(public http: Http) { } GetNodes(parentId: number) { return this.http.get("/api/tree/" + parentId.toString()) .map(res=> res.json()); } }
      
      







結果は、そのような「ツリービュー」フレームワークです。 将来的には、選択用のアイコンのプロパティを作成して、ブートストラップ3からツリービューを切り離すことができます。



バックエンドについては説明しませんが、そこには興味深いものは何もありません。通常のWeb APIコントローラーとエンティティフレームワークです。



次のテスト対象は、asp:ListViewです。 私のプロジェクトでは、あらゆる場所であらゆる方法で使用されています。 組み込みの挿入、更新テンプレートあり、なし、複数の並べ替え、ページング、フィルターあり...



更新1:

ご意見ありがとうございます。 それらに基づいて、コンポーネントはわずかに変更されました。

isExpandedフィールドとその処理を追加しました。 メソッドの数を減らしました。



treeview.component.ts ver:0.2
 import {Component, Input, Output, EventEmitter} from 'angular2/core'; export interface ITreeNode { id: number; name: string; children: Array<ITreeNode>; isExpanded: boolean; } @Component({ selector: "tree-view", templateUrl: "/app/components/treeview/treeview.html", directives: [TreeViewComponent] }) export class TreeViewComponent { @Input() Nodes: Array<ITreeNode>; @Input() SelectedNode: ITreeNode; @Output() onSelectedChanged: EventEmitter<ITreeNode> = new EventEmitter(); @Output() onRequestNodes: EventEmitter<ITreeNode> = new EventEmitter(); constructor() { } onSelectNode(node: ITreeNode) { this.onSelectedChanged.emit(node); } onExpand(node: ITreeNode) { node.isExpanded = !node.isExpanded; if (node.isExpanded && node.children.length == 0) { this.onRequestNodes.emit(parent); } } }
      
      







treeview.html ver:0.2
 <ul class="treenodes"> <li *ngFor="#node of Nodes" class="treenode"> <i class="nodebutton fa fa-{{node.isExpanded ? 'minus' : 'plus'}}-square-o" (click)="onExpand(node)"> </i> <span class="nodetext {{node == SelectedNode ? 'bg-info' : ''}}" (click)="onSelectNode(node)"> {{node.name}} </span> <tree-view [Nodes]="node.children" [SelectedNode]="SelectedNode" (onSelectedChanged)="onSelectNode($event)" (onRequestNodes)="onRequest($event)" *ngIf="node.isExpanded"> </tree-view> </li> </ul>
      
      









アップデート2:

Angular 2のリリースに関連して、現在のバージョンはすでに2.2.0であるため、コンポーネントの現在のバージョンを投稿することにしました。

主な変更:





treeview.html ver:0.3
 import {Component, Input, Output, EventEmitter} from "@angular/core"; export interface ITreeNode { id: number; name: string; children: Array<ITreeNode>; isExpanded: boolean; badge: number; parent: ITreeNode; isLeaf: boolean; } @Component({ selector: "tree-view", template: ` <ul class="treenodes"> <li *ngFor="let node of Nodes" class="treenode"> <i *ngIf="!node.isLeaf" class="nodebutton fa fa-{{node.isExpanded ? 'minus' : 'plus'}}-square-o" (click)="onExpand(node)"> </i> <div class="nodeinfo"> <i *ngIf="node.isLeaf" class="nodeicon fa fa-file-o"></i> <i *ngIf="!node.isLeaf" class="nodeicon fa fa-tags"></i> <span class="nodetext {{node == SelectedNode ? 'bg-info' : ''}} {{node.parent ? '' : 'text-root'}}" (click)="onSelectNode(node)"> {{node.name}} </span> <span *ngIf="node.badge > 0" class="nodebage badge">{{node.badge}}</span> <tree-view [Nodes]="node.children" [SelectedNode]="SelectedNode" (onSelectedChanged)="onSelectNode($event)" (onRequestNodes)="onRequestLocal($event)" *ngIf="node.isExpanded"> </tree-view> </div> </li> </ul> `, styles: [ '.treenodes {display:table; list-style-type: none; padding-left: 16px;}', ':host .treenodes { padding-left: 0; }', '.treenode { display: table-row; list-style-type: none; }', '.nodebutton { display:table-cell; cursor: pointer; }', '.nodeinfo { display:table-cell; padding-left: 5px; list-style-type: none; }', '.nodetext { color: #31708f; padding-left: 3px; padding-right: 3px; cursor: pointer; }', '.nodetext.bg-info { font-weight: bold; }', '.nodetext.text-root { font-size: 16px; font-weight: bold; }' ] }) export class TreeView { @Input() Nodes: Array<ITreeNode>; @Input() SelectedNode: ITreeNode; @Output() onSelectedChanged: EventEmitter<ITreeNode> = new EventEmitter<ITreeNode>(); @Output() onRequestNodes: EventEmitter<ITreeNode> = new EventEmitter<ITreeNode>(); constructor() { } onSelectNode(node: ITreeNode) { this.onSelectedChanged.emit(node); } onExpand(node: ITreeNode) { node.isExpanded = !node.isExpanded; if (node.isExpanded && (!node.children || node.children.length === 0)) { this.onRequestNodes.emit(node); } } onRequestLocal(node: ITreeNode) { this.onRequestNodes.emit(node); } }
      
      









建設的な批判の準備ができています。



All Articles