Можно ли сделать Tree View с Angular?
Я ищу для отображения данных в виде древовидной структуры в веб-приложении. Я надеялся использовать Angular для этой задачи.
Похоже, что ng-repeat позволит мне перебирать список узлов, но как я могу тогда делать вложение, когда глубина данного узла увеличивается?
Я попробовал следующий код, но авто-экранирование HTML не позволяет этому работать. Кроме того, конечный тег ul находится не в том месте.
Я уверен, что я иду по этой проблеме совершенно неправильно.
Есть идеи?
14 ответов
Взгляни на эту скрипку
Оригинал: http://jsfiddle.net/brendanowen/uXbn6/8/
Обновлено: http://jsfiddle.net/animaxf/uXbn6/4779/
Это должно дать вам хорошее представление о том, как отображать tree like structure
используя угловой. Это своего рода использование рекурсии в HTML!
Если вы используете Bootstrap CSS...
Я создал простой многоразовый древовидный элемент управления (директива) для AngularJS на основе списка "nav" в Bootstrap. Я добавил дополнительные отступы, иконки и анимацию. Атрибуты HTML используются для конфигурации.
Он не использует рекурсию.
Я назвал это angular-bootstrap-nav-tree (броское имя, вы не думаете?)
Здесь есть пример, а источник здесь.
При создании чего-то подобного лучшим решением является рекурсивная директива. Однако, когда вы делаете такую директиву, вы обнаруживаете, что AngularJS попадает в бесконечный цикл.
Решением для этого является позволить директиве удалить элемент во время события компиляции, а затем вручную скомпилировать и добавить их в события ссылки.
Я узнал об этом в этой теме и абстрагировал эту функциональность в сервис.
module.factory('RecursionHelper', ['$compile', function($compile){
return {
/**
* Manually compiles the element, fixing the recursion loop.
* @param element
* @param [link] A post-link function, or an object with function(s) registered via pre and post properties.
* @returns An object containing the linking functions.
*/
compile: function(element, link){
// Normalize the link parameter
if(angular.isFunction(link)){
link = { post: link };
}
// Break the recursion loop by removing the contents
var contents = element.contents().remove();
var compiledContents;
return {
pre: (link && link.pre) ? link.pre : null,
/**
* Compiles and re-adds the contents
*/
post: function(scope, element){
// Compile the contents
if(!compiledContents){
compiledContents = $compile(contents);
}
// Re-add the compiled contents to the element
compiledContents(scope, function(clone){
element.append(clone);
});
// Call the post-linking function, if any
if(link && link.post){
link.post.apply(null, arguments);
}
}
};
}
};
}]);
С помощью этого сервиса вы можете легко создать директиву дерева (или другие рекурсивные директивы). Вот пример директивы дерева:
module.directive("tree", function(RecursionHelper) {
return {
restrict: "E",
scope: {family: '='},
template:
'<p>{{ family.name }}</p>'+
'<ul>' +
'<li ng-repeat="child in family.children">' +
'<tree family="child"></tree>' +
'</li>' +
'</ul>',
compile: function(element) {
return RecursionHelper.compile(element);
}
};
});
Посмотрите этот Plunker для демонстрации. Мне больше нравится это решение, потому что:
- Вам не нужна специальная директива, которая делает ваш HTML менее чистым.
- Логика рекурсии абстрагируется от службы RecursionHelper, поэтому вы сохраняете свои директивы в чистоте.
Обновление: добавлена поддержка пользовательских функций связывания.
Вот пример использования рекурсивной директивы: http://jsfiddle.net/n8dPm/ Взято из https://groups.google.com/forum/
module.directive("tree", function($compile) {
return {
restrict: "E",
scope: {family: '='},
template:
'<p>{{ family.name }}</p>'+
'<ul>' +
'<li ng-repeat="child in family.children">' +
'<tree family="child"></tree>' +
'</li>' +
'</ul>',
compile: function(tElement, tAttr) {
var contents = tElement.contents().remove();
var compiledContents;
return function(scope, iElement, iAttr) {
if(!compiledContents) {
compiledContents = $compile(contents);
}
compiledContents(scope, function(clone, scope) {
iElement.append(clone);
});
};
}
};
});
Этот кажется немного более полным: https://github.com/dump247/angular.tree
Другой пример, основанный на исходном источнике, с образцовой древовидной структурой (уже видно, как работает IMO) и фильтром для поиска в дереве:
Так много отличных решений, но я чувствую, что все они так или иначе слишком усложняют вещи.
Я хотел создать что-то, что воссоздает простоту awnser @Mark Lagendijk, но без определения шаблона в директиве, а скорее позволил бы "пользователю" создать шаблон в HTML...
С идеями, взятыми с https://github.com/stackfull/angular-tree-repeat т. Д.... В итоге я создал проект: https://github.com/dotJEM/angular-tree
Который позволяет вам построить свое дерево, как:
<ul dx-start-with="rootNode">
<li ng-repeat="node in $dxPrior.nodes">
{{ node.name }}
<ul dx-connect="node"/>
</li>
</ul>
Что для меня чище, чем создавать несколько директив для деревьев с разной структурой... По сути, называть вышеприведенное дерево немного ложным, оно намного лучше извлекает из "рекурсивных шаблонов" @ ganaraj, но позволяет нам определить шаблон, где нам нужно дерево.
(Вы могли бы сделать это с помощью шаблона на основе тега сценария, но он все еще должен находиться прямо за узлом фактического дерева, и он все еще чувствует себя немного юк...)
Оставленный здесь только для другого выбора...
Вы можете попробовать с образцом Angular-Tree-DnD с Angular-Ui-Tree, но я отредактировал, совместимость с таблицей, сеткой, списком.
- Способный Drag & Drop
- Директива расширенной функции для списка (next, prev, getChildren,...)
- Фильтровать данные.
- OrderBy (ver)
Основываясь на ответе @ganaraj и ответе @dnc253, я просто сделал простую "директиву" для древовидной структуры с возможностью выбора, добавления, удаления и редактирования.
Jsfiddle: http://jsfiddle.net/yoshiokatsuneo/9dzsms7y/
HTML:
<script type="text/ng-template" id="tree_item_renderer.html">
<div class="node" ng-class="{selected: data.selected}" ng-click="select(data)">
<span ng-click="data.hide=!data.hide" style="display:inline-block; width:10px;">
<span ng-show="data.hide && data.nodes.length > 0" class="fa fa-caret-right">+</span>
<span ng-show="!data.hide && data.nodes.length > 0" class="fa fa-caret-down">-</span>
</span>
<span ng-show="!data.editting" ng-dblclick="edit($event)" >{{data.name}}</span>
<span ng-show="data.editting"><input ng-model="data.name" ng-blur="unedit()" ng-focus="f()"></input></span>
<button ng-click="add(data)">Add node</button>
<button ng-click="delete(data)" ng-show="data.parent">Delete node</button>
</div>
<ul ng-show="!data.hide" style="list-style-type: none; padding-left: 15px">
<li ng-repeat="data in data.nodes">
<recursive><sub-tree data="data"></sub-tree></recursive>
</li>
</ul>
</script>
<ul ng-app="Application" style="list-style-type: none; padding-left: 0">
<tree data='{name: "Node", nodes: [],show:true}'></tree>
</ul>
JavaScript:
angular.module("myApp",[]);
/* https://stackru.com/a/14657310/1309218 */
angular.module("myApp").
directive("recursive", function($compile) {
return {
restrict: "EACM",
require: '^tree',
priority: 100000,
compile: function(tElement, tAttr) {
var contents = tElement.contents().remove();
var compiledContents;
return function(scope, iElement, iAttr) {
if(!compiledContents) {
compiledContents = $compile(contents);
}
compiledContents(scope,
function(clone) {
iElement.append(clone);
});
};
}
};
});
angular.module("myApp").
directive("subTree", function($timeout) {
return {
restrict: 'EA',
require: '^tree',
templateUrl: 'tree_item_renderer.html',
scope: {
data: '=',
},
link: function(scope, element, attrs, treeCtrl) {
scope.select = function(){
treeCtrl.select(scope.data);
};
scope.delete = function() {
scope.data.parent.nodes.splice(scope.data.parent.nodes.indexOf(scope.data), 1);
};
scope.add = function() {
var post = scope.data.nodes.length + 1;
var newName = scope.data.name + '-' + post;
scope.data.nodes.push({name: newName,nodes: [],show:true, parent: scope.data});
};
scope.edit = function(event){
scope.data.editting = true;
$timeout(function(){event.target.parentNode.querySelector('input').focus();});
};
scope.unedit = function(){
scope.data.editting = false;
};
}
};
});
angular.module("myApp").
directive("tree", function(){
return {
restrict: 'EA',
template: '<sub-tree data="data" root="data"></sub-tree>',
controller: function($scope){
this.select = function(data){
if($scope.selected){
$scope.selected.selected = false;
}
data.selected = true;
$scope.selected = data;
};
},
scope: {
data: '=',
}
}
});
Когда древовидная структура большая, Angular (до 1.4.x) становится очень медленным при рендеринге рекурсивного шаблона. Попробовав ряд этих предложений, я создал простую строку HTML и использовал ng-bind-html
показать это. Конечно, это не способ использовать угловые функции
Здесь показана рекурсивная функция голых костей (с минимальным HTML):
function menu_tree(menu, prefix) {
var html = '<div>' + prefix + menu.menu_name + ' - ' + menu.menu_desc + '</div>\n';
if (!menu.items) return html;
prefix += menu.menu_name + '/';
for (var i=0; i<menu.items.length; ++i) {
var item = menu.items[i];
html += menu_tree(item, prefix);
}
return html;
}
// Generate the tree view and tell Angular to trust this HTML
$scope.html_menu = $sce.trustAsHtml(menu_tree(menu, ''));
В шаблоне нужна только одна строка:
<div ng-bind-html="html_menu"></div>
Это обходит все привязки данных Angular и просто отображает HTML за долю времени рекурсивных шаблонных методов.
С такой структурой меню (частичное дерево файлов файловой системы Linux):
menu = {menu_name: '', menu_desc: 'root', items: [
{menu_name: 'bin', menu_desc: 'Essential command binaries', items: [
{menu_name: 'arch', menu_desc: 'print machine architecture'},
{menu_name: 'bash', menu_desc: 'GNU Bourne-Again SHell'},
{menu_name: 'cat', menu_desc: 'concatenate and print files'},
{menu_name: 'date', menu_desc: 'display or set date and time'},
{menu_name: '...', menu_desc: 'other files'}
]},
{menu_name: 'boot', menu_desc: 'Static files of the boot loader'},
{menu_name: 'dev', menu_desc: 'Device files'},
{menu_name: 'etc', menu_desc: 'Host-specific system configuration'},
{menu_name: 'lib', menu_desc: 'Essential shared libraries and kernel modules'},
{menu_name: 'media', menu_desc: 'Mount point for removable media'},
{menu_name: 'mnt', menu_desc: 'Mount point for mounting a filesystem temporarily'},
{menu_name: 'opt', menu_desc: 'Add-on application software packages'},
{menu_name: 'sbin', menu_desc: 'Essential system binaries'},
{menu_name: 'srv', menu_desc: 'Data for services provided by this system'},
{menu_name: 'tmp', menu_desc: 'Temporary files'},
{menu_name: 'usr', menu_desc: 'Secondary hierarchy', items: [
{menu_name: 'bin', menu_desc: 'user utilities and applications'},
{menu_name: 'include', menu_desc: ''},
{menu_name: 'local', menu_desc: '', items: [
{menu_name: 'bin', menu_desc: 'local user binaries'},
{menu_name: 'games', menu_desc: 'local user games'}
]},
{menu_name: 'sbin', menu_desc: ''},
{menu_name: 'share', menu_desc: ''},
{menu_name: '...', menu_desc: 'other files'}
]},
{menu_name: 'var', menu_desc: 'Variable data'}
]
}
Выход становится:
- root
/bin - Essential command binaries
/bin/arch - print machine architecture
/bin/bash - GNU Bourne-Again SHell
/bin/cat - concatenate and print files
/bin/date - display or set date and time
/bin/... - other files
/boot - Static files of the boot loader
/dev - Device files
/etc - Host-specific system configuration
/lib - Essential shared libraries and kernel modules
/media - Mount point for removable media
/mnt - Mount point for mounting a filesystem temporarily
/opt - Add-on application software packages
/sbin - Essential system binaries
/srv - Data for services provided by this system
/tmp - Temporary files
/usr - Secondary hierarchy
/usr/bin - user utilities and applications
/usr/include -
/usr/local -
/usr/local/bin - local user binaries
/usr/local/games - local user games
/usr/sbin -
/usr/share -
/usr/... - other files
/var - Variable data
Да, это определенно возможно. Вопрос здесь, вероятно, предполагает Angular 1.x, но для дальнейшего использования я включаю пример Angular 2:
Концептуально все, что вам нужно сделать, это создать рекурсивный шаблон:
<ul>
<li *for="#dir of directories">
<span><input type="checkbox" [checked]="dir.checked" (click)="dir.check()" /></span>
<span (click)="dir.toggle()">{{ dir.name }}</span>
<div *if="dir.expanded">
<ul *for="#file of dir.files">
{{file}}
</ul>
<tree-view [directories]="dir.directories"></tree-view>
</div>
</li>
</ul>
Затем вы связываете объект дерева с шаблоном и позволяете Angular творить чудеса. Эта концепция, очевидно, применима и к Angular 1.x.
Вот полный пример: http://www.syntaxsuccess.com/viewarticle/recursive-treeview-in-angular-2.0
Для этого вы можете использовать угловой рекурсивный инжектор: https://github.com/knyga/angular-recursion-injector
Позволяет делать неограниченную глубину вложенности с кондиционированием. Выполняет перекомпиляцию только при необходимости и компилирует только нужные элементы. Никакой магии в коде.
<div class="node">
<span>{{name}}</span>
<node--recursion recursion-if="subNode" ng-model="subNode"></node--recursion>
</div>
Одна из вещей, которая позволяет ему работать быстрее и проще, чем другие решения, это суффикс "--recursion".
Не сложно.
<div ng-app="Application" ng-controller="TreeController">
<table>
<thead>
<tr>
<th>col 1</th>
<th>col 2</th>
<th>col 3</th>
</tr>
</thead>
<tbody ng-repeat="item in tree">
<tr>
<td>{{item.id}}</td>
<td>{{item.fname}}</td>
<td>{{item.lname}}</td>
</tr>
<tr ng-repeat="children in item.child">
<td style="padding-left:15px;">{{children.id}}</td>
<td>{{children.fname}}</td>
</tr>
</tbody>
</table>
</div>
код контроллера:
angular.module("myApp", []).
controller("TreeController", ['$scope', function ($scope) {
$scope.tree = [{
id: 1,
fname: "tree",
child: [{
id: 1,
fname: "example"
}],
lname: "grid"
}];
}]);