Ricorsione in direttive angolari


178

Ci sono un paio di popolari domande e risposte sulla direttiva angolare ricorsiva, che dipendono tutte da una delle seguenti soluzioni:

Il primo ha il problema che non è possibile rimuovere il codice precedentemente compilato a meno che non si gestisca in modo comprensibile il processo di compilazione manuale. Il secondo approccio ha il problema di ... non essere una direttiva e perdere le sue potenti capacità, ma più urgentemente, non può essere parametrizzato come una direttiva; è semplicemente associato a una nuova istanza del controller.

Ho giocato facendo manualmente una angular.bootstrapo @compile()nella funzione link, ma questo mi lascia il problema di tenere traccia manualmente degli elementi da rimuovere e aggiungere.

C'è un buon modo per avere un modello ricorsivo parametrizzato che gestisce l'aggiunta / rimozione di elementi per riflettere lo stato di runtime? Vale a dire, un albero con un pulsante Aggiungi / Elimina nodo e alcuni campi di input il cui valore viene passato ai nodi figlio di un nodo. Forse una combinazione del secondo approccio con ambiti concatenati (ma non ho idea di come farlo)?

Risposte:


316

Ispirato alle soluzioni descritte nel thread citato da @ dnc253, ho estratto la funzionalità di ricorsione in un servizio .

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

Quale è usato come segue:

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

Guarda questo Plunker per una demo. Mi piace di più questa soluzione perché:

  1. Non hai bisogno di una direttiva speciale che renda il tuo HTML meno pulito.
  2. La logica di ricorsione viene sottratta al servizio RecursionHelper, in modo da mantenere pulite le direttive.

Aggiornamento: da Angular 1.5.x, non sono necessari più trucchi, ma funziona solo con template , non con templateUrl


3
Grazie, ottima soluzione! davvero pulito e pronto all'uso per me per fare la ricorsione tra due direttive che includono l'un l'altro lavoro.
jssebastian,

6
Il problema originale è che quando si usano le direttive ricorsive AngularJS entra in un ciclo infinito. Questo codice interrompe questo ciclo rimuovendo i contenuti durante l'evento di compilazione della direttiva e compilando e aggiungendo nuovamente i contenuti nell'evento di collegamento della direttiva.
Mark Lagendijk,

15
Nel tuo esempio potresti sostituire compile: function(element) { return RecursionHelper.compile(element); }con compile: RecursionHelper.compile.
Paolo Moretti,

1
Cosa succede se si desidera che il modello si trovi in ​​un file esterno?
CodyBugstein

2
Questo è elegante nel senso che se / quando Angular core implementa un supporto simile, puoi semplicemente rimuovere il wrapper di compilazione personalizzato e tutto il codice rimanente rimarrebbe lo stesso.
Carlo Bonamico,

25

Aggiungere manualmente elementi e compilarli è sicuramente un approccio perfetto. Se si utilizza ng-repeat, non sarà necessario rimuovere manualmente gli elementi.

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

1
Ho aggiornato il tuo script in modo che abbia una sola direttiva. jsfiddle.net/KNM4q/103 Come possiamo far funzionare quel pulsante di eliminazione?
Benny Bottema,

Molto bella! Ero molto vicino, ma non avevo @position (pensavo di poterlo trovare con parentData [val]. Se aggiorni la tua risposta con la versione finale ( jsfiddle.net/KNM4q/111 ) la accetterò.
Benny Bottema,

12

Non so per certo se questa soluzione si trova in uno degli esempi che hai collegato o nello stesso concetto di base, ma avevo bisogno di una direttiva ricorsiva e ho trovato un'ottima soluzione facile .

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

È necessario creare la recursivedirettiva e quindi avvolgerla attorno all'elemento che effettua la chiamata ricorsiva.


1
@MarkError e @ dnc253 questo è utile, tuttavia ricevo sempre il seguente errore:[$compile:multidir] Multiple directives [tree, tree] asking for new/isolated scope on: <recursive tree="tree">
Jack

1
Se qualcun altro sta riscontrando questo errore, solo tu (o Yoeman) non hai incluso alcun file JavaScript più di una volta. In qualche modo il mio file main.js è stato incluso due volte e quindi venivano create due direttive con lo stesso nome. Dopo aver rimosso uno degli include JS, il codice ha funzionato.
Jack

2
@Jack Grazie per averlo sottolineato. Trascorri un certo numero di ore a risolvere questo problema e il tuo commento mi ha indicato la giusta direzione. Per gli utenti ASP.NET che utilizzano il servizio di raggruppamento, assicurarsi di non avere una versione ridotta di un file nella directory mentre si utilizzano i caratteri jolly inclusi nel raggruppamento.
Oltre il

Per me, l'elemento è necessario per aggiungere il callback interno come:. compiledContents(scope,function(clone) { iElement.append(clone); });Altrimenti, il controller "richiesto" non è gestito correttamente ed errore: Error: [$compile:ctreq] Controller 'tree', required by directive 'subTreeDirective', can't be found!causa.
Tsuneo Yoshioka,

Sto cercando di generare la struttura ad albero con j angolari ma bloccato con quello.
Learning-Overthinker-Confused

10

A partire da Angular 1.5.x, non sono necessari più trucchi, è stato reso possibile quanto segue. Non c'è più bisogno di lavori sporchi!

Questa scoperta è stata un sottoprodotto della mia caccia a una soluzione migliore / più pulita per una direttiva ricorsiva. Puoi trovarlo qui https://jsfiddle.net/cattails27/5j5au76c/ . Supporta fino a 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>


1
Grazie per questo. Potresti collegarmi al log delle modifiche che ha introdotto questa funzione? Grazie!
Steven,

L'uso dell'angolare 1.5.x è molto importante. 1.4.x non funzionerà ed è effettivamente la versione fornita in jsfiddle.
Paqman,

nel jsfiddle jsfiddle.net/cattails27/5j5au76c non esiste lo stesso codice di questa risposta ... è giusto? cosa mi manca?
Paolo Biavati,

Il violino mostra versioni angolari inferiori a 1,5x
jkris

4

Dopo aver utilizzato diverse soluzioni alternative per un po ', sono tornato più volte a questo problema.

Non sono soddisfatto della soluzione di servizio poiché funziona per direttive che possono iniettare il servizio ma non funziona per frammenti di modello anonimi.

Allo stesso modo, le soluzioni che dipendono dalla specifica struttura del modello mediante manipolazione del DOM nella direttiva sono troppo specifiche e fragili.

Ho quella che credo sia una soluzione generica che incapsula la ricorsione come una direttiva a sé stante che interferisce minimamente con qualsiasi altra direttiva e può essere utilizzata in modo anonimo.

Di seguito è una dimostrazione con cui puoi giocare anche su 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="angular.js@1.3.15" 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>


2

Ora che Angular 2.0 è uscito in anteprima, penso che sia giusto aggiungere un'alternativa Angular 2.0 al mix. Almeno andrà a beneficio delle persone in seguito:

Il concetto chiave è costruire un modello ricorsivo con un riferimento personale:

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

Quindi associare un oggetto albero al modello e guardare la ricorsione a occuparsi del resto. Ecco un esempio completo: http://www.syntaxsuccess.com/viewarticle/recursive-treeview-in-angular-2.0


2

C'è una soluzione davvero molto semplice per questo che non richiede affatto direttive.

Bene, in questo senso, forse non è nemmeno una soluzione del problema originale se si presume di aver bisogno di direttive, ma È una soluzione se si desidera una struttura GUI ricorsiva con sottostrutture parametrizzate della GUI. Che è probabilmente quello che vuoi.

La soluzione si basa solo sull'uso di ng-controller, ng-init e ng-include. Fallo come segue, supponi che il tuo controller sia chiamato "MyController", che il tuo modello si trovi in ​​myTemplate.html e che tu abbia una funzione di inizializzazione sul tuo controller chiamata init che accetta gli argomenti A, B e C, rendendo possibile parametrizzare il controller. Quindi la soluzione è la seguente:

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>

Ho scoperto per pura coincidenza che questo tipo di struttura può essere resa ricorsiva a piacimento nella semplice vaniglia angolare. Basta seguire questo modello di progettazione e puoi usare strutture UI ricorsive senza alcun armeggiamento di compilazione avanzato ecc.

All'interno del 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
} 

L'unico aspetto negativo che posso vedere è la sintassi goffa che devi sopportare.


Temo che questo non risolva il problema in un modo piuttosto fondamentale: con questo approccio dovresti conoscere la profondità della ricorsione in anticipo per avere abbastanza controller in myTemplate.html
Stewart_R

In realtà no. Poiché il tuo file myTemplate.html contiene un riferimento personale a myTemplate.html usando ng-include (il contenuto html sopra è il contenuto di myTemplate.html, forse non chiaramente indicato). In questo modo diventa veramente ricorsivo. Ho usato la tecnica in produzione.
Erobwen,

Inoltre, forse non chiaramente indicato è che è necessario utilizzare anche ng-if da qualche parte per terminare la ricorsione. Quindi il mio myTemplate.html ha quindi la forma aggiornata nel mio commento.
Erobwen,

0

A tale scopo è possibile utilizzare l'iniettore di ricorsione angolare: https://github.com/knyga/angular-recursion-injector

Consente di eseguire annidamenti di profondità illimitati con condizionamento. Compila solo se necessario e compila solo gli elementi giusti. Nessuna magia nel codice.

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

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

Una delle cose che gli permette di lavorare più velocemente e più facilmente delle altre soluzioni è il suffisso "--recursion".


0

Ho finito per creare una serie di direttive di base per la ricorsione.

IMO È molto più semplice della soluzione trovata qui, e altrettanto flessibile se non di più, quindi non siamo tenuti ad usare strutture UL / LI ecc ... Ma ovviamente quelle hanno senso usare, tuttavia le direttive non ne sono consapevoli fatto...

Un esempio semplicissimo sarebbe:

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

L'implementazione di 'dx-start-with' e 'dx-connect' è disponibile su: https://github.com/dotJEM/angular-tree

Ciò significa che non è necessario creare 8 direttive se sono necessari 8 layout diversi.

Creare una vista ad albero in aggiunta a quella in cui è possibile aggiungere o eliminare nodi sarebbe quindi piuttosto semplice. Come 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>

Da questo punto in poi, il controller e il modello potrebbero essere racchiusi nella propria direttiva se lo si desidera.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.