Açısal direktiflerde özyineleme


178

Orada aşağıdaki çözümlerden birine inen birkaç popüler özyinelemeli açısal yönerge Q&A vardır:

İlki , manuel derleme işlemini kapsamlı bir şekilde yönetmediğiniz sürece daha önce derlenmiş kodu kaldıramayacağınız bir soruna sahiptir. İkinci yaklaşım , bir direktif olmama ve güçlü yeteneklerini kaçırmama sorununa sahiptir, ancak daha acil olarak, bir direktifle aynı şekilde parametrelendirilemez; sadece yeni bir denetleyici örneğine bağlıdır.

El ile bir angular.bootstrapveya @compile()bağlantı işlevini yaparken oynuyorum , ama bu beni kaldırmak ve eklemek için öğeleri el tutma sorunu ile beni bırakır.

Çalışma zamanı durumunu yansıtacak öğeleri ekleme / kaldırma işlemini yöneten parametreli bir yinelemeli kalıbın olması iyi bir yol var mı? Yani, düğüm ekle / sil düğmeli bir ağaç ve değeri bir düğümün alt düğümlerinden geçirilen bazı giriş alanları. Belki ikinci yaklaşımın zincirleme kapsamlarla bir kombinasyonu (ama bunu nasıl yapacağım hakkında hiçbir fikrim yok)?

Yanıtlar:


316

@ Dnc253 tarafından belirtilen iplikte açıklanan çözümlerden esinlenerek, özyineleme işlevini bir hizmete soyutladım .

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

Hangi aşağıdaki gibi kullanılır:

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

Bir demo için bu Plunker'a bakın . Bu çözümü en çok seviyorum çünkü:

  1. Html'nizi daha az temiz yapan özel bir yönerge gerekmez.
  2. Özyineleme mantığı, RecursionHelper hizmetine kaldırılır, böylece yönergelerinizi temiz tutarsınız.

Güncelleme: Açısal 1.5.x itibariyle artık hile gereklidir, ancak yalnızca çalışır şablona sahip değil, templateUrl


3
Teşekkürler, harika bir çözüm! gerçekten temiz ve birbirini içeren iki direktif arasında özyineleme yapmak için kutunun dışında çalıştı.
jssebastian

6
Özgün sorun özyinelemeli yönergeler kullandığınızda AngularJS sonsuz bir döngü içine alır olmasıdır. Bu kod, direktifin compile olayı sırasında içeriği kaldırarak ve direktifin link olayında içeriği derleyip yeniden ekleyerek bu döngüyü kırar.
Mark Lagendijk

15
Örneğinizde compile: function(element) { return RecursionHelper.compile(element); }ile değiştirebilirsiniz compile: RecursionHelper.compile.
Paolo Moretti

1
Şablonun harici bir dosyada bulunmasını isterseniz ne olur?
CodyBugstein

2
Bu, Açısal çekirdek benzer bir destek uyguladığında / uyguladığında, özel derleme paketini kaldırabileceğiniz ve kalan tüm kodların aynı kalacağı açısından zariftir.
Carlo Bonamico

25

Elemanları manuel olarak eklemek ve derlemek kesinlikle mükemmel bir yaklaşımdır. Ng-repeat kullanıyorsanız, elemanları manuel olarak kaldırmanız gerekmez.

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
Komut dosyanızı yalnızca bir yönerge içerecek şekilde güncelledim. jsfiddle.net/KNM4q/103 Bu silme düğmesinin nasıl çalışmasını sağlayabiliriz?
Benny Bottema

Çok hoş! Çok yakın, ama (ben parentData [val] ile bulabiliriz düşünce size (son sürümü ile cevap güncellerseniz @position yoktu. Jsfiddle.net/KNM4q/111 ) Bunu kabul edeceğiz.
Benny Bottema

12

Bu çözümün bağladığınız örneklerden birinde mi yoksa aynı temel kavramda mı bulunduğundan emin değilim, ancak özyinelemeli bir yönerge ihtiyacım vardı ve harika, kolay bir çözüm buldum .

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

Yönergeyi oluşturmanız recursiveve ardından özyinelemeli çağrıyı yapan öğenin etrafına sarmanız gerekir.


1
@MarkError ve @ dnc253 bu yardımcı olur, ancak her zaman aşağıdaki hatayı alıyorum:[$compile:multidir] Multiple directives [tree, tree] asking for new/isolated scope on: <recursive tree="tree">
Jack

1
Başka biri bu hatayı yaşıyorsa, yalnızca siz (veya Yoeman) birden fazla JavaScript dosyası eklemediniz. Her nasılsa main.js dosyam iki kez dahil edildi ve bu nedenle aynı ada sahip iki direktif oluşturuldu. JS içeriğinden birini kaldırdıktan sonra kod çalıştı.
Jack

2
@Jack Bunu işaret ettiğiniz için teşekkürler. Bu sorunu çekmek için birkaç saat harcayın ve yorumunuz beni doğru yöne yönlendirdi. Paketleme hizmetinden yararlanan ASP.NET kullanıcıları için, paketlemede joker karakter eklentileri kullanırken dizinde bir dosyanın küçültülmüş eski sürümüne sahip olmadığınızdan emin olun.
Beyers

: Benim için, eleman gibi iç geri arama eklemek için gerekli olan compiledContents(scope,function(clone) { iElement.append(clone); });, .Aksi takdirde ed kontrolör ele doğru değil "iste" ve hata: Error: [$compile:ctreq] Controller 'tree', required by directive 'subTreeDirective', can't be found!nedenidir.
Tsuneo Yoshioka

Açısal js ile ağaç yapısı oluşturmak için çalışıyorum ama onunla sıkışmış.
Öğrenme-Overthinker-Karışık

10

Açısal 1.5.x'ten itibaren daha fazla hile gerekmiyor, aşağıdakiler mümkün oldu. Artık kirli işlere gerek yok!

Bu keşif, özyinelemeli bir direktif için daha iyi / daha temiz bir çözüm arayışımın bir ürünüdür. Bunu burada bulabilirsiniz https://jsfiddle.net/cattails27/5j5au76c/ . 1.3.x kadar destekliyor.

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
Bunun için teşekkürler. Beni bu özelliği tanıtan değişiklik günlüğüne bağlayabilir misiniz? Teşekkürler!
Steven

Açısal 1.5.x kullanmak çok önemlidir. 1.4.x çalışmaz ve aslında jsfiddle'da sağlanan sürümdür.
Paqman

jsfiddle jsfiddle.net/cattails27/5j5au76c bu cevabın aynı kodu yok ... doğru mu? ne eksik?
Paolo Biavati

Keman,
1.5x'den

4

Bir süre birkaç geçici çözüm kullandıktan sonra, tekrar tekrar bu konuya geldim.

Hizmeti enjekte edebilir ancak anonim şablon parçaları için çalışmayan yönergeler için çalıştığı için hizmet çözümünden memnun değilim.

Benzer şekilde, direktifte DOM manipülasyonu yaparak belirli şablon yapısına bağlı olan çözümler çok spesifik ve kırılgandır.

Özyinelemeyi, özyinelemeyi, diğer yönergelerle en az düzeyde müdahale eden ve anonim olarak kullanılabilen kendi bir yönergesi olarak içine alan genel bir çözüm olduğuna inanıyorum.

Aşağıda plnkr'de de oynayabileceğiniz bir gösteri var: 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

Şimdi Angular 2.0 önizlemede çıktı. Karışıma Angular 2.0 alternatifi eklemenin uygun olduğunu düşünüyorum. En azından insanlara daha sonra fayda sağlayacak:

Anahtar kavram öz referansla özyinelemeli bir şablon oluşturmaktır:

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

Daha sonra bir ağaç nesnesini şablona bağlar ve özyinelemenin geri kalanıyla ilgilenmesini izlersiniz. İşte tam bir örnek: http://www.syntaxsuccess.com/viewarticle/recursive-treeview-in-angular-2.0


2

Bunun için direktif gerektirmeyen gerçekten çok basit bir çözüm var.

Bu anlamda, direktiflere ihtiyacınız olduğunu varsayarsanız belki de orijinal sorunun bir çözümü değildir, ancak GUI'nin parametrelenmiş alt yapılarına sahip özyinelemeli bir GUI yapısı istiyorsanız bu bir çözümdür. Muhtemelen istediğiniz şey budur.

Çözüm sadece ng-denetleyici, ng-init ve ng-include kullanımına dayanmaktadır. Bunu aşağıdaki gibi yapın, denetleyicinizin "MyController" olarak adlandırıldığını, şablonunuzun myTemplate.html dosyasında bulunduğunu ve denetleyicinizde A, B ve C bağımsız değişkenini alan init adında bir başlatma işlevi bulunduğunu varsayalım. kumandanızı parametrelendirin. Sonra çözüm aşağıdaki gibidir:

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>

Bu tür bir yapının düz vanilya açısalında istediğiniz gibi özyineli hale getirilebileceğini açıkça anladım. Sadece bu tasarım desenini takip edin ve herhangi bir gelişmiş derleme müdahalesi vb. Olmadan özyinelemeli UI yapılarını kullanabilirsiniz.

Kumandanızın içinde:

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

Görebildiğim tek dezavantajı, katlanmak zorunda olduğunuz aksak sözdizimidir.


Korkarım ki bu, sorunu oldukça temel bir şekilde çözemez: Bu yaklaşımla, myTemplate.html
Stewart_R

Aslında, bilmiyorsun. MyTemplate.html dosyanız ng-include kullanarak myTemplate.html'e kendi kendine bir referans içerdiğinden (yukarıdaki html içeriği, muhtemelen açıkça belirtilmeyen myTemplate.html içeriğidir). Bu şekilde gerçekten özyinelemeli hale gelir. Tekniği üretimde kullandım.
erobwen

Ayrıca, belki de açıkça belirtilmemişse, özyinelemeyi sonlandırmak için bir yerde ng-if kullanmanız gerekir. Bu nedenle myTemplate.html, yorumumda güncellenen formda olur.
erobwen

0

Bunun için açısal-özyineleme-enjektör kullanabilirsiniz: https://github.com/knyga/angular-recursion-injector

Koşullandırma ile sınırsız derinlik yuvalama yapmanızı sağlar. Yalnızca gerektiğinde yeniden derleme yapar ve yalnızca doğru öğeleri derler. Kodda sihir yok.

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

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

Diğer çözümlerden daha hızlı ve daha basit çalışmasını sağlayan şeylerden biri "--recursion" son ekidir.


0

Sonunda özyineleme için bir dizi temel direktif oluşturdum.

IMO Burada bulunan çözümden çok daha temel ve daha fazla olmasa da esnek, bu yüzden UL / LI yapılarını vb. Kullanmak zorunda değiliz ... Ama açıkçası bunlar kullanımı mantıklı, ancak direktifler bunun farkında değil aslında ...

Süper basit bir örnek:

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

'Dx-start-with' ve 'dx-connect' uygulamalarını şu adreste bulabilirsiniz: https://github.com/dotJEM/angular-tree

Bu, 8 farklı düzene ihtiyacınız varsa 8 direktif oluşturmanız gerekmediği anlamına gelir.

Bunun üzerine düğüm ekleyebileceğiniz veya silebileceğiniz bir ağaç görünümü oluşturmak oldukça basittir. Aşağıdaki gibi: 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>

Bu noktadan sonra, denetleyici ve şablon, isterse kendi yönergesine sarılabilir.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.