각도 지시문의 재귀


178

인기있는 재귀 각도 지시문 Q & A가 몇 가지 있습니다. 모두 다음 솔루션 중 하나에 해당합니다.

첫 번째 는 수동 컴파일 프로세스를 포괄적으로 관리하지 않으면 이전에 컴파일 된 코드를 제거 할 수 없다는 문제가 있습니다. 두 번째 방법 은 지시어가 아니고 강력한 기능을 잃어버린 문제이지만 더 긴급하게 지시어와 같은 방식으로 매개 변수를 지정할 수 없습니다. 단순히 새로운 컨트롤러 인스턴스에 바인딩됩니다.

수동으로 angular.bootstrap또는 @compile()링크 기능 을 사용하여 놀고 있지만 제거하고 추가 할 요소를 수동으로 추적하는 문제가 있습니다.

런타임 상태를 반영하기 위해 요소 추가 / 제거를 관리하는 매개 변수화 된 재귀 패턴을 갖는 좋은 방법이 있습니까? 즉, 노드 추가 / 삭제 버튼과 값이 노드의 하위 노드로 전달되는 일부 입력 필드가있는 트리입니다. 아마도 두 번째 접근법과 체인 범위를 결합했을 수도 있습니다 (그러나 어떻게 해야할지 모르겠습니다)?

답변:


316

@ dnc253이 언급 한 스레드에 설명 된 솔루션에서 영감을 얻어 재귀 기능 을 서비스로 추상화했습니다 .

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

다음과 같이 사용됩니다.

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

데모는 이 Plunker 를 참조하십시오 . 나는이 솔루션을 가장 좋아합니다.

  1. HTML을 덜 깨끗하게 만드는 특별한 지시문이 필요하지 않습니다.
  2. 재귀 논리는 RecursionHelper 서비스로 추상화되므로 지시문을 깨끗하게 유지하십시오.

업데이트 : 각도 1.5.x 이하의로는 더 이상 트릭이 필요하지 않습니다,하지만 작동 템플릿 하지, templateUrl


3
고마워, 훌륭한 솔루션! 서로를 포함하여 두 가지 지시문 사이에서 재귀를 만들 수 있도록 정말 깨끗하고 즉시 작업했습니다.
jssebastian

6
원래 문제는 재귀 지시문을 사용할 때 AngularJS가 무한 루프에 빠진다는 것입니다. 이 코드는 지시문의 컴파일 이벤트 중에 컨텐츠를 제거하고 지시문의 링크 이벤트에서 컨텐츠를 컴파일하고 다시 추가하여이 루프를 해제합니다.
Mark Lagendijk

15
귀하의 예에서는으로 바꿀 수 compile: function(element) { return RecursionHelper.compile(element); }있습니다 compile: RecursionHelper.compile.
Paolo Moretti

1
템플릿을 외부 파일에 배치하려면 어떻게합니까?
CodyBugstein

2
Angular 코어가 유사한 지원을 구현하는 경우 사용자 정의 컴파일 래퍼를 제거하면 나머지 모든 코드가 동일하게 유지됩니다.
Carlo Bonamico

25

수동으로 요소를 추가하고 컴파일하는 것은 완벽한 방법입니다. ng-repeat를 사용하면 요소를 수동으로 제거 할 필요가 없습니다.

데모 : 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
지시어가 하나만 있도록 스크립트를 업데이트했습니다. jsfiddle.net/KNM4q/103 어떻게 삭제 버튼을 작동시킬 수 있습니까?
베니 보 테마

아주 좋아요! 나는 매우 가까웠지만 @position이 없었습니다 (parentData [val]로 찾을 수 있다고 생각했습니다. 최종 버전 ( jsfiddle.net/KNM4q/111 )으로 답변을 업데이트하면 받아 들일 것입니다.
베니 보 테마

12

이 솔루션이 링크 된 예제 중 하나 또는 동일한 기본 개념에서 발견되는지 확실하지 않지만 재귀 지시문이 필요 하고 훌륭하고 쉬운 해결책을 찾았 습니다 .

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

당신은 만들어야합니다 recursive지침을 다음 재귀 호출을 만드는 요소를 랩 어라운드.


1
@MarkError 및 @ dnc253이 정보는 도움이되지만 항상 다음과 같은 오류가 발생합니다.[$compile:multidir] Multiple directives [tree, tree] asking for new/isolated scope on: <recursive tree="tree">
Jack

1
다른 사람 이이 오류를 겪고 있다면, 당신 (또는 Yoeman)은 JavaScript 파일을 두 번 이상 포함하지 않았습니다. 어떻게 든 내 main.js 파일이 두 번 포함되었으므로 동일한 이름을 가진 두 개의 지시문이 작성되었습니다. JS include 중 하나를 제거한 후 코드가 작동했습니다.
Jack

2
@ 잭 지적 해 주셔서 감사합니다. 이 문제를 해결하는 데 많은 시간을 소비하면 의견이 올바른 방향으로 안내되었습니다. 번들 서비스를 사용하는 ASP.NET 사용자의 경우, 번들에 와일드 카드 포함을 사용하는 동안 디렉토리에 파일의 오래된 축소 버전이 없어야합니다.
Beyers

나를 위해 요소는 다음과 같이 콜백 내부에 추가 compiledContents(scope,function(clone) { iElement.append(clone); });해야합니다. 그렇지 않으면 "필수"컨트롤러가 올바르게 처리되지 않고 오류 : Error: [$compile:ctreq] Controller 'tree', required by directive 'subTreeDirective', can't be found!원인.
요시오카 츠네오

각도 j로 트리 구조를 생성하려고하지만 그와 붙어 있습니다.
Learning-Overthinker-Confused

10

Angular 1.5.x부터는 더 이상 트릭이 필요하지 않으며 다음이 가능해졌습니다. 더 이상 더러운 작업 환경이 필요하지 않습니다!

이 발견은 재귀 지시문에 대한 더 나은 / 더 깨끗한 솔루션을 찾은 결과입니다. https://jsfiddle.net/cattails27/5j5au76c/에서 찾을 수 있습니다 . 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
고마워 이 기능을 소개 한 변경 로그에 저를 연결할 수 있습니까? 감사!
Steven

각도 1.5.x를 사용하는 것이 매우 중요합니다. 1.4.x는 작동하지 않으며 실제로 jsfiddle에서 제공되는 버전입니다.
Paqman

jsfiddle jsfiddle.net/cattails27/5j5au76c 에는이 답변과 동일한 코드가 없습니다 ... 맞습니까? 내가 뭘 놓친거야?
Paolo Biavati

바이올린은 1.5x 미만의 앵귤러 버전을 보여줍니다
jkris

4

잠시 동안 여러 가지 해결 방법을 사용한 후이 문제로 반복해서 돌아 왔습니다.

서비스를 주입 할 수 있지만 익명의 템플릿 조각에는 작동하지 않는 지시문에는 작동하기 때문에 서비스 솔루션에 만족하지 않습니다.

마찬가지로 지시문에서 DOM 조작을 수행하여 특정 템플릿 구조에 의존하는 솔루션은 너무 구체적이며 취성입니다.

나는 다른 지시문을 최소한으로 방해하고 익명으로 사용할 수있는 자체 지시문으로 재귀를 캡슐화하는 일반적인 솔루션이라고 생각합니다.

아래는 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

이제 Angular 2.0이 미리보기에 나오기 때문에 Angular 2.0 대안을 믹스에 추가해도 괜찮습니다. 적어도 나중에 사람들에게 도움이 될 것입니다.

핵심 개념은 자체 참조로 재귀 템플릿을 작성하는 것입니다.

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

그런 다음 트리 개체를 템플릿에 바인딩하고 재귀가 나머지를 처리하는 것을 지켜보십시오. 전체 예는 다음과 같습니다. http://www.syntaxsuccess.com/viewarticle/recursive-treeview-in-angular-2.0


2

지시문이 전혀 필요없는 정말 간단한 해결 방법이 있습니다.

그런 의미에서 지시어가 필요하다고 가정하면 원래 문제의 해결책조차 아닐 수도 있지만 GUI의 하위 구조로 매개 변수가있는 재귀 GUI 구조를 원한다면 해결책입니다. 아마 당신이 원하는 것입니다.

해결책은 ng-controller, ng-init 및 ng-include를 사용하는 것입니다. 컨트롤러를 "MyController"라고하고 템플릿이 myTemplate.html에 있고 인수 A, B 및 C를 취하는 init라는 컨트롤러에 초기화 함수가 있다고 가정하면 다음과 같이하십시오. 컨트롤러를 매개 변수화하십시오. 그런 다음 해결책은 다음과 같습니다.

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>

나는 분명히 바닐라 각 에서처럼 이런 종류의 구조를 재귀 적으로 만들 수 있음을 알았습니다. 이 디자인 패턴을 따르기 만하면 고급 컴파일 땜질 등이없는 재귀 UI 구조를 사용할 수 있습니다.

컨트롤러 내부 :

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

내가 볼 수있는 유일한 단점은 당신이 참 아야 할 괴상한 구문입니다.


이것이 근본적인 방법으로 문제를 해결하지 못하는 것이 두렵습니다.이 방법을 사용하면 myTemplate.html에 충분한 컨트롤러를 갖기 위해 재귀의 깊이를 미리 알아야합니다.
Stewart_R

사실, 당신은하지 않습니다. myTemplate.html 파일에 ng-include를 사용하여 myTemplate.html에 대한 자체 참조가 포함되어 있기 때문에 (위의 html 내용은 myTemplate.html의 내용이며, 명확하게 언급되지 않았을 수 있습니다). 그렇게하면 진정 재귀가됩니다. 나는 생산 기술을 사용했다.
erobwen

또한 재귀를 종료하려면 ng-if를 사용해야 할 수도 있습니다. 따라서 myTemplate.html은 내 의견에 업데이트 된 형태입니다.
erobwen

0

이를 위해 angular-recursion-injector를 사용할 수 있습니다 : https://github.com/knyga/angular-recursion-injector

컨디셔닝으로 깊이 심도 중첩을 수행 할 수 있습니다. 필요한 경우에만 재 컴파일하고 올바른 요소 만 컴파일합니다. 코드에 마법이 없습니다.

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

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

다른 솔루션보다 더 빠르고 간단하게 작동 할 수있는 것 중 하나는 "-재귀"접미사입니다.


0

결국 재귀에 대한 기본 지시문 집합을 만들었습니다.

IMO 여기에서 찾은 솔루션보다 훨씬 더 기본적이고, 더 유연하지는 않지만 유연성이 뛰어나므로 UL / LI 구조 등을 사용할 수는 없습니다. 그러나 분명히 사용하는 것이 합리적이지만 지침은이를 인식하지 못합니다. 것...

매우 간단한 예는 다음과 같습니다.

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

'dx-start-with'의 'dx-connect'구현은 https://github.com/dotJEM/angular-tree에 있습니다.

즉, 8 개의 다른 레이아웃이 필요한 경우 8 개의 지시문을 만들 필요가 없습니다.

노드를 추가하거나 삭제할 수있는 트리 뷰를 작성하는 것은 다소 간단합니다. 에서와 같이 : 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>

이 시점부터 컨트롤러와 템플릿은 원하는 경우 자체 지시문에 래핑 될 수 있습니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.