Пишу TreeView на Angular 2

в 10:13, , рубрики: angular 2.0, angular2-component, angular2-service, AngularJS, javascript, treeview, Веб-разработка

Вдохновившись статьей «Порог вхождения в 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:

image

Процесс настройки 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 и без, с множественной сортировкой, с пейджингом, с фильтрами…

Готов к конструктивной критике.

Автор: supersmeh

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js