Рекурсия в угловых директивах
есть несколько популярных рекурсивных угловых директив Q&A, которые все сводятся к одному из следующих решений:
- вручную постепенно "компилировать" HTML на основе состояния области выполнения
- не используйте директиву вообще, но шаблон , который относится к себе
первый есть проблема, что вы не можете удалить ранее скомпилированный код, если вы не понятно управлять процессом ручной компиляции. второй подход есть проблема... не будучи директивой и упуская свои мощные возможности, но более срочно, он не может быть параметризован так же, как директива может быть; он просто привязан к новому экземпляру контроллера.
Я играл с вручную делать
angular.bootstrapили@compile()в функции link, но это оставляет меня с проблемой ручного отслеживания элементов для удаления и добавления.
есть ли хороший способ иметь параметризованный рекурсивный шаблон, который управляет добавлением / удалением элементов для отражения состояния выполнения? То есть дерево с кнопкой добавления / удаления узла и некоторым полем ввода, значение которого передается вниз дочерние узлы узла. Возможно, сочетание второго подхода с цепными областями (но я понятия не имею, как это сделать)?
9 ответов:
вдохновленный решениями, описанными в потоке, упомянутом @dnc253, я абстрагировал функциональность рекурсии в службу.
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", ["RecursionHelper", 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) { // Use the compile function from the RecursionHelper, // And return the linking function(s) which it returns return RecursionHelper.compile(element); } }; }]);посмотреть этот Plunker для демонстрации. Мне нравится это решение лучше, потому что:
- вам не нужна специальная директива, которая делает ваш HTML-менее чисто.
- логика рекурсии абстрагируется в RecursionHelper обслуживание, поэтому вы держите ваши директивы в чистоте.
ручное добавление элементов и их компиляция, безусловно, идеальный подход. Если вы используете ng-repeat, то вам не придется вручную удалять элементы.
Demo:http://jsfiddle.net/KNM4q/113/
.directive('tree', function ($compile) { return { restrict: 'E', terminal: true, scope: { val: '=', parentData:'=' }, link: function (scope, element, attrs) { var template = '<span>{{val.text}}</span>'; template += '<button ng-click="deleteMe()" ng-show="val.text">delete</button>'; if (angular.isArray(scope.val.items)) { template += '<ul class="indent"><li ng-repeat="item in val.items"><tree val="item" parent-data="val.items"></tree></li></ul>'; } scope.deleteMe = function(index) { if(scope.parentData) { var itemIndex = scope.parentData.indexOf(scope.val); scope.parentData.splice(itemIndex,1); } scope.val = {}; }; var newElement = angular.element(template); $compile(newElement)(scope); element.replaceWith(newElement); } } });
Я не знаю точно, Найдено ли это решение в одном из примеров, которые вы связали, или в той же базовой концепции, но мне нужна была рекурсивная директива, и я нашел отличное, простое решение.
module.directive("recursive", function($compile) { return { restrict: "EACM", priority: 100000, compile: function(tElement, tAttr) { var contents = tElement.contents().remove(); var compiledContents; return function(scope, iElement, iAttr) { if(!compiledContents) { compiledContents = $compile(contents); } iElement.append( compiledContents(scope, function(clone) { return clone; })); }; } }; }); module.directive("tree", function() { return { scope: {tree: '='}, template: '<p>{{ tree.text }}</p><ul><li ng-repeat="child in tree.children"><recursive><span tree="child"></span></recursive></li></ul>', compile: function() { return function() { } } }; });вы должны создать
recursiveдиректива, а затем обернуть его вокруг элемента, который делает рекурсивный вызов.
по состоянию на угловой 1.5.x, больше никаких трюков не требуется, следующее стало возможным. Нет больше необходимости в грязной работе вокруг!
это открытие было результатом моей охоты за лучшим/более чистым решением для рекурсивной директивы. Вы можете найти его здесь https://jsfiddle.net/cattails27/5j5au76c/. он поддерживает, насколько это 1.3.x.
angular.element(document).ready(function() { angular.module('mainApp', []) .controller('mainCtrl', mainCtrl) .directive('recurv', recurveDirective); angular.bootstrap(document, ['mainApp']); function recurveDirective() { return { template: '<ul><li ng-repeat="t in tree">{{t.sub}}<recurv tree="t.children"></recurv></li></ul>', scope: { tree: '=' }, } } }); function mainCtrl() { this.tree = [{ title: '1', sub: 'coffee', children: [{ title: '2.1', sub: 'mocha' }, { title: '2.2', sub: 'latte', children: [{ title: '2.2.1', sub: 'iced latte' }] }, { title: '2.3', sub: 'expresso' }, ] }, { title: '2', sub: 'milk' }, { title: '3', sub: 'tea', children: [{ title: '3.1', sub: 'green tea', children: [{ title: '3.1.1', sub: 'green coffee', children: [{ title: '3.1.1.1', sub: 'green milk', children: [{ title: '3.1.1.1.1', sub: 'black tea' }] }] }] }] }]; }<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.8/angular.min.js"></script> <div> <div ng-controller="mainCtrl as vm"> <recurv tree="vm.tree"></recurv> </div> </div>
после использования нескольких обходных путей на некоторое время, я неоднократно возвращался к этой проблеме.
Я не удовлетворен решением службы, так как он работает для директив, которые могут вводить службу, но не работает для анонимных фрагментов шаблона.
аналогичным образом, решения, которые зависят от конкретной структуры шаблона, выполняя манипуляции DOM в директиве, слишком специфичны и хрупки.
у меня есть то, что я считаю общим решением, которое инкапсулирует рекурсию как собственную директиву, которая минимально вмешивается в любые другие директивы и может использоваться анонимно.
Ниже приведена демонстрация, с которой вы также можете поиграть в plnkr:http://plnkr.co/edit/MSiwnDFD81HAOXWvQWIM
var hCollapseDirective = function () { return { link: function (scope, elem, attrs, ctrl) { scope.collapsed = false; scope.$watch('collapse', function (collapsed) { elem.toggleClass('collapse', !!collapsed); }); }, scope: {}, templateUrl: 'collapse.html', transclude: true } } var hRecursiveDirective = function ($compile) { return { link: function (scope, elem, attrs, ctrl) { ctrl.transclude(scope, function (content) { elem.after(content); }); }, controller: function ($element, $transclude) { var parent = $element.parent().controller('hRecursive'); this.transclude = angular.isObject(parent) ? parent.transclude : $transclude; }, priority: 500, // ngInclude < hRecursive < ngIf < ngRepeat < ngSwitch require: 'hRecursive', terminal: true, transclude: 'element', $$tlb: true // Hack: allow multiple transclusion (ngRepeat and ngIf) } } angular.module('h', []) .directive('hCollapse', hCollapseDirective) .directive('hRecursive', hRecursiveDirective)/* Demo CSS */ * { box-sizing: border-box } html { line-height: 1.4em } .task h4, .task h5 { margin: 0 } .task { background-color: white } .task.collapse { max-height: 1.4em; overflow: hidden; } .task.collapse h4::after { content: '...'; } .task-list { padding: 0; list-style: none; } /* Collapse directive */ .h-collapse-expander { background: inherit; position: absolute; left: .5px; padding: 0 .2em; } .h-collapse-expander::before { content: '•'; } .h-collapse-item { border-left: 1px dotted black; padding-left: .5em; } .h-collapse-wrapper { background: inherit; padding-left: .5em; position: relative; }<!DOCTYPE html> <html> <head> <link href="collapse.css" rel="stylesheet" /> <link href="style.css" rel="stylesheet" /> <script data-require="[email protected]" data-semver="1.3.15" src="https://code.angularjs.org/1.3.15/angular.js"></script> <script src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.1.1/jquery.min.js" data-semver="2.1.1" data-require="jquery@*"></script> <script src="script.js"></script> <script> function AppController($scope) { $scope.toggleCollapsed = function ($event) { $event.preventDefault(); $event.stopPropagation(); this.collapsed = !this.collapsed; } $scope.task = { name: 'All tasks', assignees: ['Citizens'], children: [ { name: 'Gardening', assignees: ['Gardeners', 'Horticulture Students'], children: [ { name: 'Pull weeds', assignees: ['Weeding Sub-committee'] } ], }, { name: 'Cleaning', assignees: ['Cleaners', 'Guests'] } ] } } angular.module('app', ['h']) .controller('AppController', AppController) </script> </head> <body ng-app="app" ng-controller="AppController"> <h1>Task Application</h1> <p>This is an AngularJS application that demonstrates a generalized recursive templating directive. Use it to quickly produce recursive structures in templates.</p> <p>The recursive directive was developed in order to avoid the need for recursive structures to be given their own templates and be explicitly self-referential, as would be required with ngInclude. Owing to its high priority, it should also be possible to use it for recursive directives (directives that have templates which include the directive) that would otherwise send the compiler into infinite recursion.</p> <p>The directive can be used alongside ng-if and ng-repeat to create recursive structures without the need for additional container elements.</p> <p>Since the directive does not request a scope (either isolated or not) it should not impair reasoning about scope visibility, which continues to behave as the template suggests.</p> <p>Try playing around with the demonstration, below, where the input at the top provides a way to modify a scope attribute. Observe how the value is visible at all levels.</p> <p>The collapse directive is included to further demonstrate that the recursion can co-exist with other transclusions (not just ngIf, et al) and that sibling directives are included on the recursive due to the recursion using whole 'element' transclusion.</p> <label for="volunteer">Citizen name:</label> <input id="volunteer" ng-model="you" placeholder="your name"> <h2>Tasks</h2> <ul class="task-list"> <li class="task" h-collapse h-recursive> <h4>{{task.name}}</h4> <h5>Volunteers</h5> <ul> <li ng-repeat="who in task.assignees">{{who}}</li> <li>{{you}} (you)</li> </ul> <ul class="task-list"> <li h-recursive ng-repeat="task in task.children"></li> </ul> <li> </ul> <script type="text/ng-template" id="collapse.html"> <div class="h-collapse-wrapper"> <a class="h-collapse-expander" href="#" ng-click="collapse = !collapse"></a> <div class="h-collapse-item" ng-transclude></div> </div> </script> </body> </html>
теперь, когда Angular 2.0 находится в предварительном просмотре, я думаю, что можно добавить альтернативу Angular 2.0 в микс. По крайней мере, это принесет пользу людям позже:
ключевая концепция заключается в создании рекурсивного шаблона с собственной ссылкой:
<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>затем вы привязываете объект дерева к шаблону и смотрите, как рекурсия позаботится об остальном. Вот полный пример: http://www.syntaxsuccess.com/viewarticle/recursive-treeview-in-angular-2.0
существует действительно очень простой обходной путь для этого, который не требует директив вообще.
Ну, в этом смысле, возможно, это даже не решение исходной проблемы, если вы предполагаете, что вам нужны директивы, но это решение, если вы хотите рекурсивную структуру GUI с параметризованными подструктурами GUI. Вероятно, что вы хотите.
решение основано на использовании только ng-контроллера, ng-init и ng-include. Просто сделайте это следующим образом, предположим, что ваш контроллер называется "MyController", ваш шаблон находится в myTemplate.html и что у вас есть функция инициализации на вашем контроллере с именем init, которая принимает аргумент A, B и C, что позволяет параметризовать ваш контроллер. Тогда решение выглядит следующим образом:
myTemplate.htlm:
<div> <div>Hello</div> <div ng-if="some-condition" ng-controller="Controller" ng-init="init(A, B, C)"> <div ng-include="'myTemplate.html'"></div> </div> </div>Я нашел простой совпадение, что такая структура может быть сделано рекурсивно, как вам нравится в обычные угловые. Просто следуйте этой схеме проектирования, и вы может использовать рекурсивные UI-структуры без какой-либо предварительной компиляции и т. д.
внутри вашего контроллера:
$scope.init = function(A, B, C) { // Do something with A, B, C $scope.D = A + B; // D can be passed on to other controllers in myTemplate.html }единственный недостаток, который я вижу, это неуклюжий синтаксис, с которым вам приходится мириться.
вы можете использовать angular-recursion-injector для этого: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".
в итоге я создал набор базовых директив для рекурсии.
ИМО это гораздо более простой, чем решение, найденное здесь, и так же гибко, если не больше, поэтому мы не обязаны использовать структуры UL/LI и т. д... Но очевидно, что они имеют смысл использовать, однако директивы не знают об этом факте...
простой пример:
<ul dx-start-with="rootNode"> <li ng-repeat="node in $dxPrior.nodes"> {{ node.name }} <ul dx-connect="node"/> </li> </ul>реализация 'DX-start-with' и 'dx-connect' находится по адресу: https://github.com/dotJEM/angular-tree
Это означает, что вам не нужно создавать 8 директив, если вам нужно 8 различных макетов.
чтобы создать древовидное представление поверх этого, где вы можете добавлять или удалять узлы, было бы довольно просто. Как в: http://codepen.io/anon/pen/BjXGbY?editors=1010
angular .module('demo', ['dotjem.angular.tree']) .controller('AppController', function($window) { this.rootNode = { name: 'root node', children: [{ name: 'child' }] }; this.addNode = function(parent) { var name = $window.prompt("Node name: ", "node name here"); parent.children = parent.children || []; parent.children.push({ name: name }); } this.removeNode = function(parent, child) { var index = parent.children.indexOf(child); if (index > -1) { parent.children.splice(index, 1); } } });<div ng-app="demo" ng-controller="AppController as app"> HELLO TREE <ul dx-start-with="app.rootNode"> <li><button ng-click="app.addNode($dxPrior)">Add</button></li> <li ng-repeat="node in $dxPrior.children"> {{ node.name }} <button ng-click="app.removeNode($dxPrior, node)">Remove</button> <ul dx-connect="node" /> </li> </ul> <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.0/angular.min.js"></script> <script src="https://rawgit.com/dotJEM/angular-tree-bower/master/dotjem-angular-tree.min.js"></script> </div>С этого момента контроллер и шаблон могут быть завернуты в собственную директиву, если можно было бы пожелать этого.
Comments