Rekurencja w dyrektywach kątowych

178

Istnieje kilka popularnych rekurencyjnych wytycznych dotyczących kątowych pytań i odpowiedzi, które sprowadzają się do jednego z następujących rozwiązań:

Pierwszy ma problem polegający na tym, że nie możesz usunąć wcześniej skompilowanego kodu, chyba że w sposób zrozumiały zarządzasz procesem ręcznej kompilacji. Drugie podejście polega na tym, że ... nie jest dyrektywą i nie wykorzystuje swoich potężnych możliwości, ale co pilniejsze, nie można jej sparametryzować w taki sam sposób, jak dyrektywę; jest po prostu związany z nową instancją kontrolera.

Grałem z ręcznym wykonaniem funkcji angular.bootstraplub @compile()w funkcji linku, ale to pozostawia mi problem z ręcznym śledzeniem elementów do usunięcia i dodania.

Czy istnieje dobry sposób na sparametryzowany wzorzec rekurencyjny, który zarządza dodawaniem / usuwaniem elementów w celu odzwierciedlenia stanu środowiska uruchomieniowego? To znaczy drzewo z przyciskiem dodawania / usuwania węzła i pewnym polem wejściowym, którego wartość jest przekazywana w dół do węzłów potomnych węzła. Być może połączenie drugiego podejścia z powiązanymi zakresami (ale nie mam pojęcia, jak to zrobić)?

Benny Bottema
źródło

Odpowiedzi:

316

Zainspirowany rozwiązaniami opisanymi w wątku wspomnianym przez @ dnc253, wyodrębniłem funkcjonalność rekurencji w usługę .

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

Który jest używany w następujący sposób:

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

Zobacz ten Plunker, aby zobaczyć demo. Najbardziej podoba mi się to rozwiązanie, ponieważ:

  1. Nie potrzebujesz specjalnej dyrektywy, która sprawi, że twój HTML będzie mniej czysty.
  2. Logika rekurencji zostaje wyodrębniona w usłudze RecursionHelper, więc utrzymujesz swoje dyrektywy w czystości.

Aktualizacja: Od wersji Angular 1.5.x nie są już wymagane żadne sztuczki, ale działa tylko z szablonem , a nie z szablonemUrl

Mark Lagendijk
źródło
3
Dzięki, świetne rozwiązanie! naprawdę czyste i opracowane po wyjęciu z pudełka, aby wykonać rekurencję między dwiema dyrektywami, które zawierają się nawzajem.
jssebastian
6
Pierwotny problem polega na tym, że podczas korzystania z dyrektyw rekurencyjnych AngularJS przechodzi w nieskończoną pętlę. Ten kod przerywa tę pętlę, usuwając zawartość podczas zdarzenia kompilacji dyrektywy oraz kompilując i ponownie dodając treść w zdarzeniu link dyrektywy.
Mark Lagendijk
15
W przykładzie można zastąpić compile: function(element) { return RecursionHelper.compile(element); }z compile: RecursionHelper.compile.
Paolo Moretti
1
Co zrobić, jeśli chcesz, aby szablon znajdował się w pliku zewnętrznym?
CodyBugstein,
2
Jest to eleganckie w tym sensie, że jeśli / kiedy rdzeń Angular implementuje podobne wsparcie, możesz po prostu usunąć niestandardowe opakowanie kompilacji, a cały pozostały kod pozostanie taki sam.
Carlo Bonamico
25

Ręczne dodawanie elementów i ich kompilacja to zdecydowanie idealne podejście. Jeśli użyjesz ng-repeat, nie będziesz musiał ręcznie usuwać elementów.

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
źródło
1
Zaktualizowałem twój skrypt, aby miał tylko jedną dyrektywę. jsfiddle.net/KNM4q/103 Jak sprawić, by przycisk usuwania działał?
Benny Bottema
Bardzo dobrze! Byłem bardzo blisko, ale nie miałem @position (myślałem, że mogę go znaleźć z parentData [val]. Jeśli zaktualizujesz swoją odpowiedź do ostatecznej wersji ( jsfiddle.net/KNM4q/111 ), zaakceptuję.
Benny Bottema
12

Nie wiem na pewno, czy to rozwiązanie znajduje się w jednym z przykładów, które podłączyłeś, czy w tej samej podstawowej koncepcji, ale potrzebowałem rekurencyjnej dyrektywy i znalazłem świetne, łatwe rozwiązanie .

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

Powinieneś utworzyć recursivedyrektywę, a następnie owinąć ją wokół elementu, który wykonuje wywołanie rekurencyjne.

dnc253
źródło
1
@MarkError i @ dnc253 jest to pomocne, jednak zawsze pojawia się następujący błąd:[$compile:multidir] Multiple directives [tree, tree] asking for new/isolated scope on: <recursive tree="tree">
Jack
1
Jeśli ktoś napotyka ten błąd, tylko Ty (lub Yoeman) nie zawarłeś żadnych plików JavaScript więcej niż raz. Jakoś mój plik main.js został dołączony dwukrotnie i dlatego tworzone były dwie dyrektywy o tej samej nazwie. Po usunięciu jednego z JS kod działał.
Jack
2
@Jack Dzięki za zwrócenie na to uwagi. Spędź tylko kilka godzin na rozwiązywaniu tego problemu, a Twój komentarz wskazał mi właściwy kierunek. W przypadku użytkowników ASP.NET korzystających z usługi pakietowania upewnij się, że nie masz starej zminimalizowanej wersji pliku w katalogu, gdy używasz symboli wieloznacznych w pakiecie.
Beyers,
Dla mnie element jest potrzebny do dodania wewnętrznego wywołania zwrotnego, takiego jak compiledContents(scope,function(clone) { iElement.append(clone); });:. W przeciwnym razie kontroler „wymaga” nie jest poprawnie obsługiwany, a błąd: Error: [$compile:ctreq] Controller 'tree', required by directive 'subTreeDirective', can't be found!przyczyna.
Tsuneo Yoshioka
Próbuję wygenerować strukturę drzewa za pomocą kątowego js, ​​ale utknąłem z tym.
Uczenie się nadmiernego myślenia -
10

Od wersji Angular 1.5.x nie są już wymagane żadne lewy, możliwe stało się: Koniec z brudną pracą!

To odkrycie było produktem mojego poszukiwania lepszego / czystszego rozwiązania rekurencyjnej dyrektywy. Można go znaleźć tutaj https://jsfiddle.net/cattails27/5j5au76c/ . Obsługuje do tej pory 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
źródło
1
Dzięki za to. Czy możesz połączyć mnie ze dziennikiem zmian, który wprowadził tę funkcję? Dzięki!
Steven
Używanie kątowej 1.5.x jest bardzo ważne. 1.4.x nie będzie działać i jest w rzeczywistości wersją podaną w jsfiddle.
Paqman,
w jsfiddle jsfiddle.net/cattails27/5j5au76c nie ma tego samego kodu tej odpowiedzi ... prawda? czego mi brakuje?
Paolo Biavati,
Skrzypce pokazuje wersje kątowe mniejsze niż 1,5x
jkris
4

Po dłuższym użyciu kilku obejść wielokrotnie wracałem do tego problemu.

Rozwiązanie nie usatysfakcjonuje mnie, ponieważ działa w przypadku dyrektyw, które mogą wstrzykiwać usługę, ale nie działa w przypadku anonimowych fragmentów szablonów.

Podobnie rozwiązania, które zależą od konkretnej struktury szablonu poprzez manipulację DOM w dyrektywie, są zbyt specyficzne i kruche.

Mam, jak sądzę, ogólne rozwiązanie, które zawiera rekursję jako własną dyrektywę, która w minimalnym stopniu koliduje z innymi dyrektywami i może być używana anonimowo.

Poniżej znajduje się demonstracja, z którą możesz się również pobawić na stronie 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>

tilgovi
źródło
2

Teraz, gdy Angular 2.0 jest już dostępny w wersji zapoznawczej, myślę, że można dodać do miksu alternatywę Angular 2.0. Przynajmniej przyniesie to ludziom później:

Kluczową koncepcją jest zbudowanie szablonu rekurencyjnego z własnym odniesieniem:

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

Następnie powiąż obiekt drzewa z szablonem i obserwuj, jak rekurencja zajmie się resztą. Oto pełny przykład: http://www.syntaxsuccess.com/viewarticle/recursive-treeview-in-angular-2.0

TGH
źródło
2

Istnieje naprawdę bardzo proste obejście tego problemu, które w ogóle nie wymaga dyrektyw.

Cóż, w tym sensie może nie jest to nawet rozwiązanie pierwotnego problemu, jeśli założymy, że potrzebujesz dyrektyw, ale JEST to rozwiązanie, jeśli chcesz rekurencyjną strukturę GUI ze sparametryzowanymi podstrukturami GUI. Co jest prawdopodobnie tym, czego chcesz.

Rozwiązanie opiera się na użyciu ng-kontrolera, ng-init i ng-include. Po prostu zrób to w następujący sposób, zakładając, że twój kontroler nazywa się „MyController”, twój szablon znajduje się w myTemplate.html i że masz na sterowniku funkcję inicjującą o nazwie init, która przyjmuje argumenty A, B i C, umożliwiając sparametryzuj kontroler. Następnie rozwiązanie jest następujące:

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>

Przekonałem się, że tego rodzaju struktura może być rekurencyjna, tak jak lubisz, w zwykłym waniliowym kanciastym kształcie. Wystarczy postępować zgodnie z tym wzorcem projektowym, aby korzystać z rekurencyjnych struktur interfejsu użytkownika bez zaawansowanego majsterkowania przy kompilacji itp.

Wewnątrz kontrolera:

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

Jedynym minusem, jaki widzę, jest niezgrabna składnia, z którą musisz się pogodzić.

erobwen
źródło
Obawiam się, że to nie rozwiązuje problemu w dość fundamentalny sposób: przy takim podejściu musisz znać głębokość rekurencji z góry, aby mieć wystarczająco dużo kontrolerów w myTemplate.html
Stewart_R
Właściwie nie. Ponieważ plik myTemplate.html zawiera własne odwołanie do myTemplate.html przy użyciu ng-include (powyższa zawartość HTML to zawartość myTemplate.html, być może nie jest wyraźnie określona). W ten sposób staje się naprawdę rekurencyjny. Użyłem tej techniki w produkcji.
erobwen
Ponadto, być może nie jest jasno określone, że musisz również użyć ng-jeśli gdzieś, aby zakończyć rekurencję. Zatem plik myTemplate.html ma wtedy formę zaktualizowaną w moim komentarzu.
erobwen
0

W tym celu można użyć wtryskiwacza kątowego-rekurencyjnego: https://github.com/knyga/angular-recursion-injector

Umożliwia wykonywanie nieograniczonego zagnieżdżania z uwarunkowaniem. Wykonuje rekompilację tylko w razie potrzeby i kompiluje tylko odpowiednie elementy. Brak magii w kodzie.

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

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

Jedną z rzeczy, która pozwala mu działać szybciej i prościej niż inne rozwiązania, jest przyrostek „--recursion”.

Oleksandr Knyga
źródło
0

Ostatecznie stworzyłem zestaw podstawowych dyrektyw dotyczących rekurencji.

IMO Jest to o wiele bardziej podstawowe niż rozwiązanie tutaj znalezione i tak samo elastyczne, jeśli nie bardziej, więc nie jesteśmy zobowiązani do korzystania ze struktur UL / LI itp. Ale oczywiście mają one sens, jednak dyrektywy nie są tego świadome fakt...

Bardzo prosty przykład to:

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

Implementację „dx-start-with” i „dx-connect” można znaleźć na stronie: stronie https://github.com/dotJEM/angular-tree

Oznacza to, że nie musisz tworzyć 8 dyrektyw, jeśli potrzebujesz 8 różnych układów.

Utworzenie widoku drzewa ponad tym, w którym można dodawać lub usuwać węzły, byłoby wtedy dość proste. Jak w: 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>

Od tego momentu kontroler i szablon mogą być zawinięte we własną dyrektywę, jeśli ktoś sobie tego życzy.

Jens
źródło