Recursão em diretivas angulares

178

Existem algumas perguntas e respostas populares sobre diretiva angular recursiva disponíveis, que se resumem a uma das seguintes soluções:

O primeiro tem o problema de que você não pode remover o código compilado anteriormente, a menos que você gerencie de maneira abrangente o processo de compilação manual. A segunda abordagem tem o problema de ... não ser uma diretiva e perder suas poderosas capacidades, mas mais urgentemente, ela não pode ser parametrizada da mesma maneira que uma diretiva; é simplesmente vinculado a uma nova instância do controlador.

Eu tenho brincado com a execução manual de uma função angular.bootstrapou @compile()no link, mas isso me deixa com o problema de acompanhar manualmente os elementos a serem removidos e adicionados.

Existe uma boa maneira de ter um padrão recursivo parametrizado que gerencia a adição / remoção de elementos para refletir o estado de tempo de execução? Ou seja, uma árvore com um botão de adicionar / excluir nó e algum campo de entrada cujo valor é passado pelos nós filhos de um nó. Talvez uma combinação da segunda abordagem com escopos encadeados (mas não tenho idéia de como fazer isso)?

Benny Bottema
fonte

Respostas:

316

Inspirado nas soluções descritas no encadeamento mencionado por @ dnc253, abstraii a funcionalidade de recursão em um serviço .

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

Que é usado da seguinte maneira:

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

Veja este Plunker para uma demonstração. Eu gosto mais desta solução porque:

  1. Você não precisa de uma diretiva especial que torne seu html menos limpo.
  2. A lógica de recursão é abstraída para o serviço RecursionHelper, para que você mantenha suas diretivas limpas.

Atualização: A partir do Angular 1.5.x, não são mais necessários truques, mas funciona apenas com o modelo , não com o templateUrl

Mark Lagendijk
fonte
3
Obrigado, ótima solução! realmente limpo e elaborado para que eu faça recursão entre duas diretivas que incluem uma à outra.
Jssebastian
6
O problema original é que, quando você usa diretivas recursivas, o AngularJS entra em um loop infinito. Esse código interrompe esse loop removendo o conteúdo durante o evento de compilação da diretiva e compilando e adicionando novamente o conteúdo no evento de link da diretiva.
precisa
15
No seu exemplo, você pode substituir compile: function(element) { return RecursionHelper.compile(element); }por compile: RecursionHelper.compile.
Paolo Moretti
1
E se você quiser que o modelo esteja localizado em um arquivo externo?
CodyBugstein #
2
Isso é elegante no sentido de que, se / quando o núcleo Angular implementar um suporte semelhante, você poderá remover o wrapper de compilação personalizado e todo o código restante permanecerá o mesmo.
Carlo Bonamico 15/05
25

Adicionar e compilar elementos manualmente é definitivamente uma abordagem perfeita. Se você usar ng-repeat, não precisará remover elementos manualmente.

Demonstração: 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
fonte
1
Eu atualizei seu script para que ele tenha apenas uma diretiva. jsfiddle.net/KNM4q/103 Como podemos fazer esse botão de exclusão funcionar?
precisa saber é o seguinte
Muito agradável! Eu estava muito perto, mas não tinha @position (achei que poderia encontrá-lo com parentData [val]. Se você atualizar sua resposta com a versão final ( jsfiddle.net/KNM4q/111 ), eu aceito.
Benny Bottema
12

Não sei ao certo se essa solução foi encontrada em um dos exemplos que você vinculou ou no mesmo conceito básico, mas eu precisava de uma diretiva recursiva e encontrei uma solução ótima e fácil .

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

Você deve criar a recursivediretiva e envolvê-la no elemento que faz a chamada recursiva.

dnc253
fonte
1
@MarkError e @ dnc253 isso é útil, no entanto eu sempre receber o seguinte erro:[$compile:multidir] Multiple directives [tree, tree] asking for new/isolated scope on: <recursive tree="tree">
Jack
1
Se alguém mais estiver enfrentando esse erro, apenas você (ou Yoeman) não incluiu nenhum arquivo JavaScript mais de uma vez. De alguma forma, meu arquivo main.js. foi incluído duas vezes e, portanto, duas diretivas com o mesmo nome estavam sendo criadas. Depois de remover uma das inclusões JS, o código funcionou.
31413 Jack
2
@ Jack Obrigado por apontar isso. Basta passar algumas horas com problemas para solucionar esse problema e seu comentário me indicou a direção certa. Para usuários do ASP.NET que usam o serviço de agregação, verifique se você não possui uma versão minificada antiga de um arquivo no diretório enquanto usa inclusões curinga na agregação.
Beyers 5/12/13
Para mim, é necessário adicionar um elemento para adicionar retorno de chamada interno, como:. compiledContents(scope,function(clone) { iElement.append(clone); });Caso contrário, o controlador "require" ed não é tratado corretamente e error: Error: [$compile:ctreq] Controller 'tree', required by directive 'subTreeDirective', can't be found!cause.
Tsuneo Yoshioka
Eu estou tentando gerar estrutura de árvore com js angulares, mas preso com isso.
Apr-Overthinker-Confused #
10

A partir do Angular 1.5.x, não são mais necessários truques, o seguinte foi possível. Não há mais necessidade de trabalho sujo!

Essa descoberta foi um subproduto da minha busca por uma solução melhor / mais limpa para uma diretiva recursiva. Você pode encontrá-lo aqui https://jsfiddle.net/cattails27/5j5au76c/ . Suporta até 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
fonte
1
Obrigado por isso. Você poderia me vincular ao changelog que introduziu esse recurso? Obrigado!
28816 Steven
Usar o angular 1.5.x é muito importante. O 1.4.x não funciona e é realmente a versão fornecida no jsfiddle.
Paqman 01/09/16
no jsfiddle jsfiddle.net/cattails27/5j5au76c não existe o mesmo código dessa resposta ... está certo? o que estou perdendo?
Paolo Biavati
Os shows violino para versões angulares inferior a 1,5x
jkris
4

Depois de usar várias soluções alternativas por um tempo, voltei repetidamente a esse problema.

Não estou satisfeito com a solução de serviço, pois funciona para diretivas que podem injetar o serviço, mas não para fragmentos de modelos anônimos.

Da mesma forma, as soluções que dependem da estrutura específica do modelo ao manipular o DOM na diretiva são muito específicas e quebradiças.

Eu tenho o que acredito ser uma solução genérica que encapsula a recursão como uma diretiva própria que interfere minimamente com outras diretivas e pode ser usada anonimamente.

Abaixo está uma demonstração com a qual você também pode brincar em 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
fonte
2

Agora que o Angular 2.0 está em pré-visualização, acho que é bom adicionar uma alternativa do Angular 2.0 ao mix. Pelo menos, beneficiará as pessoas mais tarde:

O conceito principal é criar um modelo recursivo com uma referência própria:

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

Em seguida, você vincula um objeto de árvore ao modelo e observa a recursão cuidar do resto. Aqui está um exemplo completo: http://www.syntaxsuccess.com/viewarticle/recursive-treeview-in-angular-2.0

TGH
fonte
2

Existe uma solução realmente muito simples para isso, que não requer diretrizes.

Bem, nesse sentido, talvez nem seja uma solução do problema original se você assumir que precisa de diretivas, mas É uma solução se você quiser uma estrutura de GUI recursiva com subestruturas parametrizadas da GUI. Qual é provavelmente o que você quer.

A solução é baseada no uso de ng-controller, ng-init e ng-include. Faça o seguinte, suponha que seu controlador seja chamado "MyController", seu modelo esteja localizado em myTemplate.html e que você tenha uma função de inicialização no seu controlador chamada init, que utilize os argumentos A, B e C, possibilitando parametrize seu controlador. Então a solução é a seguinte:

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>

Descobri, por pura conincidência, que esse tipo de estrutura pode ser recursivo como você preferir na angular simples de baunilha. Basta seguir este padrão de design e você pode usar estruturas de interface do usuário recursivas sem qualquer modificação avançada de compilação etc.

Dentro do seu controlador:

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

A única desvantagem que vejo é a sintaxe desajeitada que você precisa suportar.

erobwen
fonte
Receio que isso não consiga resolver o problema de uma maneira bastante fundamental: com essa abordagem, você precisaria conhecer a profundidade da recursão antecipada para ter controladores suficientes em myTemplate.html #
5995 Stewart_R
Na verdade você não. Como o arquivo myTemplate.html contém uma auto-referência ao myTemplate.html usando ng-include (o conteúdo html acima é o conteúdo do myTemplate.html, talvez não esteja claramente indicado). Dessa forma, torna-se verdadeiramente recursivo. Eu usei a técnica na produção.
erobwen
Além disso, talvez não esteja claramente indicado é que você também precisa usar ng-if em algum lugar para encerrar a recursão. Portanto, o meu myTemplate.html fica do formulário atualizado no meu comentário.
erobwen
0

Você pode usar o injetor de recursão angular para isso: https://github.com/knyga/angular-recursion-injector

Permite fazer aninhamentos de profundidade ilimitados com condicionamento. Recompila apenas se necessário e compila apenas os elementos corretos. Nenhuma mágica no código.

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

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

Uma das coisas que permite que ele trabalhe mais rápido e mais simples que as outras soluções é o sufixo "--recursion".

Oleksandr Knyga
fonte
0

Acabei criando um conjunto de diretrizes básicas para recursão.

IMO É muito mais básico do que a solução encontrada aqui, e tão flexível quanto não mais, por isso não somos obrigados a usar estruturas UL / LI, etc ... Mas obviamente elas fazem sentido usar, no entanto, as diretivas não têm conhecimento disso. facto...

Um exemplo super simples seria:

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

A implementação de 'dx-start-with' an 'dx-connect' pode ser encontrada em: https://github.com/dotJEM/angular-tree

Isso significa que você não precisa criar 8 diretivas se precisar de 8 layouts diferentes.

Criar uma exibição em árvore em cima da qual você pode adicionar ou excluir nós seria bastante simples. Como em: 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>

A partir deste momento, o controlador e o modelo podem ser envolvidos em sua própria diretiva, se alguém desejar.

Jens
fonte