Rekursion in Winkelanweisungen

178

Es gibt einige beliebte Fragen und Antworten zu rekursiven Winkelanweisungen, die alle auf eine der folgenden Lösungen zurückzuführen sind:

Der erste hat das Problem, dass Sie zuvor kompilierten Code nur entfernen können, wenn Sie den manuellen Kompilierungsprozess verständlich verwalten. Der zweite Ansatz hat das Problem, ... keine Richtlinie zu sein und ihre mächtigen Fähigkeiten zu verpassen, aber dringender kann er nicht so parametrisiert werden, wie eine Richtlinie sein kann; Es ist einfach an eine neue Controller-Instanz gebunden.

Ich habe mit dem manuellen Ausführen einer angular.bootstrapoder @compile()in der Link-Funktion gespielt, aber das führt mich zu dem Problem, die zu entfernenden und hinzuzufügenden Elemente manuell zu verfolgen.

Gibt es eine gute Möglichkeit, ein parametrisiertes rekursives Muster zu erstellen, das das Hinzufügen / Entfernen von Elementen verwaltet, um den Laufzeitstatus widerzuspiegeln? Das heißt, ein Baum mit einer Schaltfläche zum Hinzufügen / Löschen von Knoten und einem Eingabefeld, dessen Wert an die untergeordneten Knoten eines Knotens weitergegeben wird. Vielleicht eine Kombination des zweiten Ansatzes mit verketteten Bereichen (aber ich habe keine Ahnung, wie das geht)?

Benny Bottema
quelle

Antworten:

315

Inspiriert von den Lösungen, die in dem von @ dnc253 erwähnten Thread beschrieben sind, habe ich die Rekursionsfunktionalität in einen Dienst abstrahiert .

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);
                    }
                }
            };
        }
    };
}]);

Welches wird wie folgt verwendet:

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);
        }
    };
}]);

In diesem Plunker finden Sie eine Demo. Diese Lösung gefällt mir am besten, weil:

  1. Sie brauchen keine spezielle Direktive, die Ihr HTML weniger sauber macht.
  2. Die Rekursionslogik wird in den RecursionHelper-Dienst abstrahiert, sodass Sie Ihre Anweisungen sauber halten.

Update: Ab Angular 1.5.x sind keine Tricks mehr erforderlich, sondern funktioniert nur mit Vorlage , nicht mit templateUrl

Mark Lagendijk
quelle
3
Danke, tolle Lösung! Wirklich sauber und sofort einsatzbereit, damit ich zwischen zwei Direktiven, die sich gegenseitig einschließen, eine Rekursion mache.
Jssebastian
6
Das ursprüngliche Problem ist, dass AngularJS bei Verwendung rekursiver Direktiven in eine Endlosschleife gerät. Dieser Code unterbricht diese Schleife, indem er den Inhalt während des Kompilierungsereignisses der Direktive entfernt und den Inhalt im Link-Ereignis der Direktive kompiliert und erneut hinzufügt.
Mark Lagendijk
15
In Ihrem Beispiel könnten Sie ersetzen compile: function(element) { return RecursionHelper.compile(element); }mit compile: RecursionHelper.compile.
Paolo Moretti
1
Was ist, wenn sich die Vorlage in einer externen Datei befinden soll?
CodyBugstein
2
Dies ist insofern elegant, als wenn / wenn Angular Core eine ähnliche Unterstützung implementiert, Sie einfach den benutzerdefinierten Kompilierungs-Wrapper entfernen können und der gesamte verbleibende Code gleich bleibt.
Carlo Bonamico
25

Das manuelle Hinzufügen und Kompilieren von Elementen ist definitiv ein perfekter Ansatz. Wenn Sie ng-repeat verwenden, müssen Sie Elemente nicht manuell entfernen.

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);
    }
}
});
SunnyShah
quelle
1
Ich habe Ihr Skript so aktualisiert, dass es nur eine Direktive enthält. jsfiddle.net/KNM4q/103 Wie können wir dafür sorgen, dass diese Schaltfläche zum Löschen funktioniert?
Benny Bottema
Sehr schön! Ich war sehr nah dran, hatte aber keine @position (ich dachte, ich könnte sie mit parentData [val] finden. Wenn Sie Ihre Antwort mit der endgültigen Version ( jsfiddle.net/KNM4q/111 ) aktualisieren, werde ich sie akzeptieren.
Benny Bottema
12

Ich weiß nicht genau, ob diese Lösung in einem der von Ihnen verlinkten Beispiele oder im selben Grundkonzept enthalten ist, aber ich brauchte eine rekursive Direktive und fand eine großartige, einfache Lösung .

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() {
            }
        }
    };
});​

Sie sollten die recursiveDirektive erstellen und dann um das Element wickeln, das den rekursiven Aufruf ausführt.

dnc253
quelle
1
@ MarkError und @ dnc253 das ist hilfreich, aber ich erhalte immer den folgenden Fehler:[$compile:multidir] Multiple directives [tree, tree] asking for new/isolated scope on: <recursive tree="tree">
Jack
1
Wenn bei jemand anderem dieser Fehler auftritt, haben nur Sie (oder Yoeman) mehr als einmal keine JavaScript-Dateien eingefügt. Irgendwie war meine Datei main.js zweimal enthalten und daher wurden zwei Direktiven mit demselben Namen erstellt. Nach dem Entfernen eines der JS-Includes funktionierte der Code.
Jack
2
@ Jack Danke, dass du darauf hingewiesen hast. Verbringen Sie einfach einige Stunden damit, dieses Problem zu beheben, und Ihr Kommentar hat mich in die richtige Richtung gelenkt. Stellen Sie für ASP.NET-Benutzer, die den Bündelungsdienst verwenden, sicher, dass Sie keine alte minimierte Version einer Datei im Verzeichnis haben, während Sie Platzhalter-Includes für die Bündelung verwenden.
Beyers
Für mich wird ein Element benötigt, um einen compiledContents(scope,function(clone) { iElement.append(clone); });internen Rückruf hinzuzufügen, z. B .: Andernfalls wird der "erforderliche" Controller nicht korrekt behandelt, und Fehler: Error: [$compile:ctreq] Controller 'tree', required by directive 'subTreeDirective', can't be found!Ursache.
Tsuneo Yoshioka
Ich versuche, eine Baumstruktur mit eckigen js zu erzeugen, bleibe aber dabei.
Learning-Overthinker-Confused
10

Ab Angular 1.5.x sind keine Tricks mehr erforderlich. Folgendes wurde ermöglicht. Keine schmutzigen Arbeiten mehr nötig!

Diese Entdeckung war ein Nebenprodukt meiner Suche nach einer besseren / saubereren Lösung für eine rekursive Direktive. Sie finden es hier https://jsfiddle.net/cattails27/5j5au76c/ . Es unterstützt soweit 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>

jkris
quelle
1
Danke dafür. Könnten Sie mich mit dem Änderungsprotokoll verknüpfen, mit dem diese Funktion eingeführt wurde? Vielen Dank!
Steven
Die Verwendung von Winkel 1.5.x ist sehr wichtig. 1.4.x funktioniert nicht und ist tatsächlich die in jsfiddle bereitgestellte Version.
Paqman
In der jsfiddle jsfiddle.net/cattails27/5j5au76c gibt es nicht den gleichen Code für diese Antwort ... ist es richtig? was fehlt mir
Paolo Biavati
Die Geige zeigt für eckige Versionen weniger als 1,5x
jkris
4

Nachdem ich eine Zeit lang mehrere Problemumgehungen verwendet habe, bin ich wiederholt auf dieses Problem zurückgekommen.

Ich bin mit der Servicelösung nicht zufrieden, da sie für Anweisungen funktioniert, die den Service injizieren können, aber nicht für anonyme Vorlagenfragmente.

In ähnlicher Weise sind Lösungen, die von einer bestimmten Vorlagenstruktur durch DOM-Manipulation in der Richtlinie abhängen, zu spezifisch und spröde.

Ich glaube, es handelt sich um eine generische Lösung, die die Rekursion als eigene Direktive zusammenfasst, die alle anderen Direktiven nur minimal beeinträchtigt und anonym verwendet werden kann.

Unten finden Sie eine Demonstration, mit der Sie auch unter plnkr herumspielen können: 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>

Tilgovi
quelle
2

Jetzt, da Angular 2.0 in der Vorschau erscheint, ist es meiner Meinung nach in Ordnung, dem Mix eine Angular 2.0-Alternative hinzuzufügen. Zumindest wird es den Menschen später zugute kommen:

Das Schlüsselkonzept besteht darin, eine rekursive Vorlage mit einer Selbstreferenz zu erstellen:

<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>

Anschließend binden Sie ein Baumobjekt an die Vorlage und beobachten, wie sich die Rekursion um den Rest kümmert. Hier ist ein vollständiges Beispiel: http://www.syntaxsuccess.com/viewarticle/recursive-treeview-in-angular-2.0

TGH
quelle
2

Hierfür gibt es eine wirklich sehr einfache Problemumgehung, für die überhaupt keine Anweisungen erforderlich sind.

In diesem Sinne ist es vielleicht nicht einmal eine Lösung des ursprünglichen Problems, wenn Sie davon ausgehen, dass Sie Anweisungen benötigen, aber es ist eine Lösung, wenn Sie eine rekursive GUI-Struktur mit parametrisierten Unterstrukturen der GUI wünschen. Welches ist wahrscheinlich, was Sie wollen.

Die Lösung basiert auf der Verwendung von ng-controller, ng-init und ng-include. Gehen Sie einfach wie folgt vor, nehmen Sie an, dass Ihr Controller "MyController" heißt, Ihre Vorlage sich in myTemplate.html befindet und dass Sie auf Ihrem Controller eine Initialisierungsfunktion namens init haben, die die Argumente A, B und C akzeptiert Parametrieren Sie Ihren Controller. Dann ist die Lösung wie folgt:

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>

Ich fand durch einfachen Zufall heraus, dass diese Art von Struktur rekursiv gemacht werden kann, wie Sie es in einfachen Vanille-Winkeln mögen. Folgen Sie einfach diesem Entwurfsmuster und Sie können rekursive UI-Strukturen ohne fortgeschrittenes Kompilieren usw. verwenden.

In Ihrem Controller:

$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
} 

Der einzige Nachteil, den ich sehen kann, ist die klobige Syntax, mit der Sie sich abfinden müssen.

erobwen
quelle
Ich fürchte, dies löst das Problem nicht auf eine ziemlich grundlegende Weise: Bei diesem Ansatz müssten Sie die Tiefe der Rekursion im Voraus kennen, um genügend Controller in myTemplate.html zu haben
Stewart_R
Eigentlich nicht. Da Ihre Datei myTemplate.html einen Selbstverweis auf myTemplate.html mit ng-include enthält (der obige HTML-Inhalt ist der Inhalt von myTemplate.html, möglicherweise nicht eindeutig angegeben). Auf diese Weise wird es wirklich rekursiv. Ich habe die Technik in der Produktion verwendet.
erobwen
Vielleicht ist auch nicht klar angegeben, dass Sie ng-if auch irgendwo verwenden müssen, um die Rekursion zu beenden. Ihre myTemplate.html hat dann die Form, wie sie in meinem Kommentar aktualisiert wurde.
erobwen
0

Sie können dafür einen Winkelrekursionsinjektor verwenden: https://github.com/knyga/angular-recursion-injector

Ermöglicht das Verschachteln mit unbegrenzter Tiefe und Konditionierung. Führt nur bei Bedarf eine Neukompilierung durch und kompiliert nur die richtigen Elemente. Keine Magie im Code.

<div class="node">
  <span>{{name}}</span>

  <node--recursion recursion-if="subNode" ng-model="subNode"></node--recursion>
</div>

Eines der Dinge, die es ermöglichen, schneller und einfacher als die anderen Lösungen zu arbeiten, ist das Suffix "--recursion".

Oleksandr Knyga
quelle
0

Am Ende habe ich eine Reihe grundlegender Anweisungen für die Rekursion erstellt.

IMO Es ist weitaus grundlegender als die hier gefundene Lösung und genauso flexibel, wenn nicht sogar flexibler, sodass wir nicht an die Verwendung von UL / LI-Strukturen usw. gebunden sind. Aber offensichtlich sind diese sinnvoll, obwohl die Richtlinien dies nicht wissen Tatsache...

Ein super einfaches Beispiel wäre:

<ul dx-start-with="rootNode">
  <li ng-repeat="node in $dxPrior.nodes">
    {{ node.name }}
    <ul dx-connect="node"/>
  </li>
</ul>

Die Implementierung von 'dx-start-with' und 'dx-connect' finden Sie unter: https://github.com/dotJEM/angular-tree

Dies bedeutet, dass Sie keine 8 Anweisungen erstellen müssen, wenn Sie 8 verschiedene Layouts benötigen.

Es wäre dann ziemlich einfach, darüber hinaus eine Baumansicht zu erstellen, in der Sie Knoten hinzufügen oder löschen können. Wie in: 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>

Von diesem Punkt an könnten der Controller und die Vorlage in eine eigene Direktive eingeschlossen werden, wenn man dies wünscht.

Jens
quelle