I am writing TreeView in Angular 2

Inspired by the article “Entry Threshold in Angular 2 - Theory and Practice” , I also decided to write an article about my torment of creativity.

I have a big project written in ASP.NET WebForms. A lot of everything was mixed in it, and gradually I stopped liking it all. I decided to try to rewrite everything on something modern. I liked Angular 2 right away, and I decided to try it. The task was defined as follows: to write a new frontend, screwing it to an existing backend, with minimal alterations to the latter. The new frontend must be UI-compatible with the old so that the end user does not notice anything.

Total we have such a stack: backend - ASP.NET Web API, Entity Framework, MS SQL; frontend - Angular 2; Bootstrap 3 theme.

Immediately show the result of TreeView:

image

I will not describe the process of setting up Angular 2 in Visual Studio, this is complete in the vast. The only thing that had to be added was the setting in web.config to redirect route requests to index.html:

piece of web.config


Everything has successfully taken off. Static files are loaded correctly, web api controllers work api, other routes are always processed by index.html.

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

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

When the node is expanded, we check for the presence of descendants; in the absence, we generate the onRequestNodes event. When a user selects a node, we generate the onSelectedChanged event. Fontawesome icons.

The component has two input parameters: Nodes - a list of nodes at a given level, SelectedNode - a node selected by the user. Two events: onSelectedChanged - change of the node selected by the user, onRequestNodes - request for nodes, if necessary. @Input parameters propagate from parent to descendants (deep into the hierarchy). @Output () events propagate from descendants to parents (outside the hierarchy). The component is recursive - each new level of the 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;
}
@Component({
	selector: "tree-view",
	templateUrl: "/app/components/treeview/treeview.html",
	directives: [TreeViewComponent]
})
export class TreeViewComponent {
	@Input() Nodes: Array;
	@Input() SelectedNode: ITreeNode;
	@Output() onSelectedChanged: EventEmitter = new EventEmitter();
	@Output() onRequestNodes: EventEmitter = 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
  • {{node.name}}


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;
	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
Remember, I have bootstrap 3.



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 is such a "frame" treeview. In the future, you can make properties for the icons, for selection, to untie the treeview from bootstrap 3.

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

The next test subject will be asp: ListView. In my project, it is used everywhere and in every way. With built-in Insert, Update templates and without, with multiple sorting, with paging, with filters ...

Update 1:
Thank you all for your comments. Based on them, the component was slightly modified.
Added the isExpanded field 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;
	isExpanded: boolean;
}
@Component({
	selector: "tree-view",
	templateUrl: "/app/components/treeview/treeview.html",
	directives: [TreeViewComponent]
})
export class TreeViewComponent {
	@Input() Nodes: Array;
	@Input() SelectedNode: ITreeNode;
	@Output() onSelectedChanged: EventEmitter = new EventEmitter();
	@Output() onRequestNodes: EventEmitter = 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
  • {{node.name}}



Update 2:
In connection with the release of Angular 2, already 2.2.0 the current version, I decided to post the current version of the component.
Major changes:
  • template and styles moved from separate files to component code
  • fields required for the project have been added to ITreeNode
  • the root nodes have a different font. intentionally.


treeview.html ver: 0.3
import {Component, Input, Output, EventEmitter} from "@angular/core";
export interface ITreeNode {
	id: number;
	name: string;
	children: Array;
	isExpanded: boolean;
	badge: number;
	parent: ITreeNode;
	isLeaf: boolean;
}
@Component({
	selector: "tree-view",
	template: `
		
  • {{node.name}} {{node.badge}}
`, 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; @Input() SelectedNode: ITreeNode; @Output() onSelectedChanged: EventEmitter = new EventEmitter(); @Output() onRequestNodes: EventEmitter = new EventEmitter(); 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.

Also popular now: