📜 ⬆️ ⬇️

I am writing a TreeView on Angular 2

Inspired by the article “Threshold of entry into Angular 2 - theory and practice,” I also decided to write an article about my own torments of creativity.

I have a big project written in ASP.NET WebForms. A lot of things are mixed in it, and gradually I have ceased to like all this. I decided to try to rewrite everything on something modern. Angular 2 I liked immediately, and I decided to try it. The task was defined as follows: write a new frontend, screwing it to an existing backend, with minimal modifications to the last. The new frontend must be UI-compatible with the old one so that the end user will not notice anything.

Total we have this stack: backend - ASP.NET Web API, Entity Framework, MS SQL; frontend - Angular 2; Bootstrap 3 theme.
')
Immediately show the result TreeView:

image

The process of setting up Angular 2 in Visual Studio will not be described, in the vastness of this full. The only thing that had to be added was the setting in web.config for redirecting route requests to index.html:

web.config piece
<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> 


All successfully took off. Static files are loaded correctly, api is processed by web api controllers, the rest of the routes are always processed by index.html.

Before starting to write endpoints, I decided to first write some WebForm's controls. Of course, ListView and FormView are most often used. But I decided to start with a simple TreeView, it is also needed in several forms.

To reduce traffic, I decided to load only the necessary tree nodes. When initializing, I request only the top level.

When a node is expanded, we check for the presence of descendants; if there is no, we generate an onRequestNodes event. When a user selects a node, we generate an onSelectedChanged event. Icons fontawesome.

The component has two incoming parameters: Nodes - the list of nodes at this level, SelectedNode - the node selected by the user. Two events: onSelectedChanged - change of the node selected by the user, onRequestNodes - request of nodes, if necessary. @Input parameters propagate from parent to descendants (deep into the hierarchy). @Output () events propagate from descendants to parents (outside the hierarchy). Component recursive - each new level of hierarchy processes its own instance of the component.

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> 


Styles made a separate file.

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


How to use:

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
I remind you that I have a bootstrap 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
The most primitive service
 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()); } } 


The result was such a "frame" treeview. In the future, you can make properties for the icons, for selection, to untie the treeview from bootstrap 3.

Backend I will not describe, there is nothing interesting, the usual web api controller and entity framework.

The following test subject will be asp: ListView. In my project, it is used everywhere and for everyone. With embedded Insert, Update templates and without, with multiple sorting, with paging, with filters ...

Update 1:
Thank you all for the comments. Based on them, I modified the component a little.
Added field isExpanded and its processing. Reduced the number of methods.

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> 



Update 2:
In connection with the release of Angular 2, the current version already 2.2.0, I decided to post the current version of the component.
Major changes:


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



Ready for constructive criticism.

Source: https://habr.com/ru/post/279045/


All Articles