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:
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:
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.
Styles made a separate file.
How to use:
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.
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:
Ready for constructive criticism.
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:
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}}
0" class="nodebage badge">{{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.