Вдохновившись статьей «Порог вхождения в Angular 2 — теория и практика», решил тоже написать статью про свои муки творчества.
У меня есть большой проект, написанный на ASP.NET WebForms. В нем намешано много всякого, и постепенно мне это всё перестало нравиться. Решил я попробовать переписать всё на чем-нибудь современном. Angular 2 мне приглянулся сразу, и я решил пробовать его. Задача определилась такая: написать новый frontend, прикрутив его к существующему backend, с минимальными переделками последнего. Новый frontend должен быть UI-совместимым со старым, чтобы конечный пользователь ничего не заметил.
Итого имеем такой стэк: backend — ASP.NET Web API, Entity Framework, MS SQL; frontend — Angular 2; тема Bootstrap 3.
Сразу покажу результат TreeView:

Процесс настройки Angular 2 в Visual Studio описывать не буду, на просторах этого полно. Единственное, что пришлось добавить, это настройку в web.config для редиректа route-запросов на index.html:
Все успешно взлетело. Статик файлы грузятся правильно, api отрабатывают контроллеры web api, остальные маршруты всегда обрабатывает index.html.
Прежде чем начинать писать конечные точки, решил сначала написать некоторые контролы-аналоги WebForm's. Чаще всего конечно используется ListView и FormView. Но начать я решил с простенького TreeView, он тоже нужен в нескольких формах.
Для уменьшения трафика решил сделать загрузку только необходимых узлов дерева. При инициализации запрашиваю только верхний ур��вень.
При раскрытии узла проверяем наличие потомков, при отсутствии генерируем событие onRequestNodes. При выделении пользователем узла — генерируем событие onSelectedChanged. Иконки fontawesome.
Компонент имеет два входящих параметра: Nodes — список узлов на данном уровне, SelectedNode — выбранный пользователем узел. Два события: onSelectedChanged — смена выбранного пользователем узла, onRequestNodes — запрос узлов, при необходимости. @Input параметры распространяются от родителя к потомкам (вглубь иерархии). @Output() события распростаняются от потомков к родителям (наружу иерархии). Компонент рекурсивный — каждый новый уровень иерархии обрабатывает свой экземпляр компонента.
Стили сделал отдельным файлом.
Как использовать:
Получился вот такой «каркас» treeview. В дальнейшем можно сделать свойства для иконок, для выделения, чтобы отвязать treeview от bootstrap 3.
Backend описывать не буду, там ничего интересного, обычный web api контроллер и entity framework.
Следующий подопытный будет asp:ListView. В моём проекте он используется повсюду и по всякому. С встроенными Insert, Update templates и без, с множественной сортировкой, с пейджингом, с фильтрами…
Update 1:
Всем спасибо за комментарии. На их основании немного доработал компонент.
Добавил поле isExpanded и его обработку. Сократил кол-во методов.
Update 2:
В связи с выходом в релиз Angular 2, уже аж 2.2.0 текущая версия, решил выложить текущую версию компонента.
Основные изменения:
Готов к конструктивной критике.
У меня есть большой проект, написанный на ASP.NET WebForms. В нем намешано много всякого, и постепенно мне это всё перестало нравиться. Решил я попробовать переписать всё на чем-нибудь современном. Angular 2 мне приглянулся сразу, и я решил пробовать его. Задача определилась такая: написать новый frontend, прикрутив его к существующему backend, с минимальными переделками последнего. Новый frontend должен быть UI-совместимым со старым, чтобы конечный пользователь ничего не заметил.
Итого имеем такой стэк: backend — ASP.NET Web API, Entity Framework, MS SQL; frontend — Angular 2; тема Bootstrap 3.
Сразу покажу результат TreeView:

Процесс настройки Angular 2 в Visual Studio описывать не буду, на просторах этого полно. Единственное, что пришлось добавить, это настройку в web.config для редиректа route-запросов на index.html:
кусок 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>
Все успешно взлетело. Статик файлы грузятся правильно, api отрабатывают контроллеры web api, остальные маршруты всегда обрабатывает index.html.
Прежде чем начинать писать конечные точки, решил сначала написать некоторые контролы-аналоги WebForm's. Чаще всего конечно используется ListView и FormView. Но начать я решил с простенького TreeView, он тоже нужен в нескольких формах.
Для уменьшения трафика решил сделать загрузку только необходимых узлов дерева. При инициализации запрашиваю только верхний ур��вень.
При раскрытии узла проверяем наличие потомков, при отсутствии генерируем событие onRequestNodes. При выделении пользователем узла — генерируем событие onSelectedChanged. Иконки fontawesome.
Компонент имеет два входящих параметра: Nodes — список узлов на данном уровне, SelectedNode — выбранный пользователем узел. Два события: 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
Напоминаю, у меня 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
Самый примитивный сервис
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());
}
}
Получился вот такой «каркас» treeview. В дальнейшем можно сделать свойства для иконок, для выделения, чтобы отвязать treeview от bootstrap 3.
Backend описывать не буду, там ничего интересного, обычный web api контроллер и entity framework.
Следующий подопытный будет asp:ListView. В моём проекте он используется повсюду и по всякому. С встроенными Insert, Update templates и без, с множественной сортировкой, с пейджингом, с фильтрами…
Update 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>
Update 2:
В связи с выходом в релиз Angular 2, уже аж 2.2.0 текущая версия, решил выложить текущую версию компонента.
Основные изменения:
- шаблон и стили переехали из отдельных файлов в код компонента
- в ITreeNode добавлены необходимые проекту поля
- у корневых узлов шрифт другой. намеренно.
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);
}
}
Готов к конструктивной критике.
