Rekursi dalam arahan Angular

178

Ada beberapa Q & A directional rekursif directional di luar sana, yang semuanya datang ke salah satu solusi berikut:

Yang pertama memiliki masalah bahwa Anda tidak dapat menghapus kode yang dikompilasi sebelumnya kecuali Anda secara komprehensif mengelola proses kompilasi manual. Pendekatan kedua memiliki masalah ... tidak menjadi arahan dan kehilangan kapabilitas yang kuat, tetapi lebih mendesak, itu tidak dapat diparameterisasi dengan cara yang sama seperti arahan bisa; itu hanya terikat ke instance controller baru.

Saya telah bermain dengan melakukan angular.bootstrapatau @compile()dalam fungsi tautan secara manual , tetapi itu membuat saya memiliki masalah untuk melacak dan menghapus elemen secara manual.

Apakah ada cara yang baik untuk memiliki pola rekursif berparameter yang mengelola elemen tambah / hapus untuk mencerminkan keadaan runtime? Dengan kata lain, pohon dengan tombol tambah / hapus simpul dan beberapa bidang input yang nilainya diturunkan dari simpul anak simpul. Mungkin kombinasi dari pendekatan kedua dengan lingkup dirantai (tapi saya tidak tahu bagaimana melakukan ini)?

Benny Bottema
sumber

Jawaban:

316

Terinspirasi oleh solusi yang dijelaskan dalam utas yang disebutkan oleh @ dnc253, saya mengabstraksi fungsi rekursi menjadi sebuah layanan .

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

Yang digunakan sebagai berikut:

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

Lihat Plunker ini untuk demo. Saya suka solusi ini karena:

  1. Anda tidak memerlukan arahan khusus yang membuat html Anda kurang bersih.
  2. Logika rekursi disarikan ke layanan RecursionHelper, sehingga Anda menjaga arahan Anda tetap bersih.

Pembaruan: Pada Angular 1.5.x, tidak ada lagi trik yang diperlukan, tetapi hanya berfungsi dengan templat , tidak dengan templatUrl

Mark Lagendijk
sumber
3
Terima kasih, solusi hebat! benar-benar bersih dan bekerja di luar kotak bagi saya untuk membuat rekursi antara dua arahan yang mencakup pekerjaan masing-masing.
jssebastian
6
Masalah aslinya adalah ketika Anda menggunakan arahan rekursif, AngularJS masuk ke loop tanpa akhir. Kode ini memecah loop ini dengan menghapus konten selama acara kompilasi direktif, dan kompilasi dan menambahkan kembali konten dalam acara tautan direktif.
Mark Lagendijk
15
Dalam contoh Anda, Anda dapat menggantinya compile: function(element) { return RecursionHelper.compile(element); }dengan compile: RecursionHelper.compile.
Paolo Moretti
1
Bagaimana jika Anda ingin templat ditempatkan di file eksternal?
CodyBugstein
2
Ini anggun dalam arti jika / ketika inti Angular mengimplementasikan dukungan yang sama, Anda bisa menghapus pembungkus kompilasi kustom dan semua kode yang tersisa akan tetap sama.
Carlo Bonamico
25

Menambahkan elemen secara manual dan mengompilasinya jelas merupakan pendekatan yang sempurna. Jika Anda menggunakan ng-repeat maka Anda tidak perlu menghapus elemen secara manual.

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
sumber
1
Saya memperbarui skrip Anda sehingga hanya memiliki satu arahan. jsfiddle.net/KNM4q/103 Bagaimana caranya agar tombol hapus itu berfungsi?
Benny Bottema
Sangat bagus! Saya sangat dekat, tetapi tidak memiliki @position (saya pikir saya bisa menemukannya dengan parentData [val]. Jika Anda memperbarui jawaban Anda dengan versi final ( jsfiddle.net/KNM4q/111 ) saya akan menerimanya.
Benny Bottema
12

Saya tidak tahu pasti apakah solusi ini ditemukan dalam salah satu contoh yang Anda tautkan atau konsep dasar yang sama, tetapi saya membutuhkan arahan rekursif, dan saya menemukan solusi yang hebat dan mudah .

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

Anda harus membuat recursivearahan dan kemudian membungkusnya di sekitar elemen yang membuat panggilan rekursif.

dnc253
sumber
1
@MarkError dan @ dnc253 ini sangat membantu, namun saya selalu menerima kesalahan berikut:[$compile:multidir] Multiple directives [tree, tree] asking for new/isolated scope on: <recursive tree="tree">
Jack
1
Jika ada orang lain yang mengalami kesalahan ini, hanya Anda (atau Yoeman) yang belum memasukkan file JavaScript lebih dari sekali. Entah bagaimana file main.js saya dimasukkan dua kali dan oleh karena itu dua arahan dengan nama yang sama sedang dibuat. Setelah menghapus salah satu dari JS termasuk, kode berfungsi.
Jack
2
@ Jack Terima kasih telah menunjukkan itu. Hanya menghabiskan beberapa jam untuk memecahkan masalah ini dan komentar Anda menunjukkan saya ke arah yang benar. Untuk pengguna ASP.NET yang menggunakan layanan bundling, pastikan Anda tidak memiliki versi kecil dari file dalam direktori saat Anda menggunakan wildcard termasuk dalam bundling.
Beyers
Bagi saya, elemen diperlukan untuk menambahkan callback di dalam seperti:. Selain itu compiledContents(scope,function(clone) { iElement.append(clone); });, "memerlukan" ed controller tidak ditangani dengan benar, dan error: Error: [$compile:ctreq] Controller 'tree', required by directive 'subTreeDirective', can't be found!penyebab.
Tsuneo Yoshioka
Saya mencoba untuk menghasilkan struktur pohon dengan sudut js tetapi terjebak dengan itu.
Learning-Overthinker-Confused
10

Pada Angular 1.5.x, tidak ada lagi trik yang diperlukan, berikut ini telah dimungkinkan. Tidak perlu lagi untuk pekerjaan kotor!

Penemuan ini adalah produk sampingan dari perburuan saya untuk solusi yang lebih baik / bersih untuk arahan rekursif. Anda dapat menemukannya di sini https://jsfiddle.net/cattails27/5j5au76c/ . Ini mendukung sejauh 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
sumber
1
Terima kasih untuk ini. Bisakah Anda menautkan saya ke changelog yang memperkenalkan fitur ini? Terima kasih!
Steven
Menggunakan sudut 1.5.x sangat penting. 1.4.x tidak akan berfungsi dan sebenarnya versi yang disediakan di jsfiddle.
Paqman
di jsfiddle jsfiddle.net/cattails27/5j5au76c tidak ada kode yang sama dengan jawaban ini ... benarkah ? apa yang saya lewatkan?
Paolo Biavati
Pertunjukan biola untuk versi sudut kurang dari 1,5x
jkris
4

Setelah menggunakan beberapa solusi untuk sementara waktu, saya berulang kali kembali ke masalah ini.

Saya tidak puas dengan solusi layanan karena berfungsi untuk arahan yang dapat menyuntikkan layanan tetapi tidak berfungsi untuk fragmen templat anonim.

Demikian pula, solusi yang bergantung pada struktur templat tertentu dengan melakukan manipulasi DOM dalam arahan terlalu spesifik dan rapuh.

Saya memiliki apa yang saya yakini sebagai solusi umum yang merangkum rekursi sebagai arahannya sendiri yang mengganggu secara minimal dengan arahan lain dan dapat digunakan secara anonim.

Di bawah ini adalah demonstrasi yang dapat Anda mainkan di 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
sumber
2

Sekarang karena Angular 2.0 sudah keluar dalam pratinjau, saya pikir tidak apa-apa untuk menambahkan alternatif Angular 2.0 ke dalam campuran. Setidaknya itu akan bermanfaat bagi orang-orang nanti:

Konsep kuncinya adalah membangun templat rekursif dengan referensi sendiri:

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

Anda kemudian mengikat objek pohon ke Templat dan menonton rekursi mengurus sisanya. Berikut ini adalah contoh lengkapnya: http://www.syntaxsuccess.com/viewarticle/recursive-treeview-in-angular-2.0

TGH
sumber
2

Ada solusi yang sangat sangat sederhana untuk ini yang tidak memerlukan arahan sama sekali.

Nah, dalam pengertian itu, mungkin itu bahkan bukan solusi dari masalah awal jika Anda menganggap Anda memerlukan arahan, tetapi itu ADALAH solusi jika Anda ingin struktur GUI rekursif dengan sub-struktur parametrized dari GUI. Mungkin itulah yang Anda inginkan.

Solusinya didasarkan pada hanya menggunakan ng-controller, ng-init dan ng-include. Lakukan saja sebagai berikut, asumsikan bahwa controller Anda disebut "MyController", template Anda terletak di myTemplate.html dan bahwa Anda memiliki fungsi inisialisasi pada controller Anda yang disebut init yang mengambil argumen A, B, dan C, sehingga memungkinkan untuk parametrize pengontrol Anda. Maka solusinya adalah sebagai berikut:

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>

Saya menemukan secara kebetulan bahwa struktur seperti ini dapat dibuat rekursif sesuai keinginan Anda dalam sudut vanila polos. Cukup ikuti pola desain ini dan Anda dapat menggunakan struktur UI rekursif tanpa kompilasi mengutak-atik dll.

Di dalam controller Anda:

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

Satu-satunya downside saya dapat melihat adalah sintaks kikuk Anda harus bertahan dengan.

erobwen
sumber
Saya khawatir ini gagal untuk menyelesaikan masalah dengan cara yang agak mendasar: Dengan pendekatan ini Anda perlu mengetahui kedalaman rekursi di muka agar memiliki cukup banyak pengontrol di myTemplate.html
Stewart_R
Sebenarnya tidak. Karena file Anda myTemplate.html berisi referensi sendiri ke myTemplate.html menggunakan ng-include (konten html di atas adalah konten myTemplate.html, mungkin tidak dinyatakan dengan jelas). Dengan begitu ia menjadi sangat rekursif. Saya telah menggunakan teknik dalam produksi.
erobwen
Juga, mungkin tidak dinyatakan dengan jelas adalah bahwa Anda juga perlu menggunakan ng-jika suatu tempat untuk mengakhiri rekursi. Jadi myTemplate.html Anda kemudian dalam bentuk sebagaimana diperbarui dalam komentar saya.
erobwen
0

Anda dapat menggunakan angular-recursion-injector untuk itu: https://github.com/knyga/angular-recursion-injector

Memungkinkan Anda melakukan sarang yang tidak terbatas dengan pengkondisian. Apakah kompilasi hanya jika diperlukan dan hanya mengkompilasi elemen yang tepat. Tidak ada keajaiban dalam kode.

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

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

Salah satu hal yang memungkinkannya bekerja lebih cepat dan lebih sederhana maka solusi lain adalah sufiks "- recursion".

Oleksandr Knyga
sumber
0

Saya akhirnya membuat satu set arahan dasar untuk rekursi.

IMO Ini jauh lebih mendasar daripada solusi yang ditemukan di sini, dan sama fleksibel jika tidak lebih, jadi kita tidak terikat untuk menggunakan struktur UL / LI dll ... Tapi jelas itu masuk akal untuk digunakan, namun arahan tidak menyadari hal ini fakta...

Contoh super sederhana adalah:

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

Implementasi 'dx-start-with' an 'dx-connect' ditemukan di: https://github.com/dotJEM/angular-tree

Ini berarti Anda tidak perlu membuat 8 arahan jika Anda membutuhkan 8 tata letak yang berbeda.

Untuk membuat tampilan pohon di atas itu di mana Anda dapat menambah atau menghapus node maka akan lebih sederhana. Seperti pada: 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>

Dari titik ini, controller dan template dapat dibungkus dengan direktifnya sendiri jika seseorang menginginkannya.

Jens
sumber