[javascript] 각도 지시문의 재귀

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

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

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

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



답변

@ 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


답변

수동으로 요소를 추가하고 컴파일하는 것은 완벽한 방법입니다. 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);
    }
}
});


답변

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

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지침을 다음 재귀 호출을 만드는 요소를 랩 어라운드.


답변

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>


답변

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

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

마찬가지로 지시문에서 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>


답변

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


답변

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

그런 의미에서 지시어가 필요하다고 가정하면 원래 문제의 해결책조차 아닐 수도 있지만 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
} 

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