3

I'm new to Angular and trying to wrap my brain around how to do stuff.

I'm trying to create a list that's populated on page load by an ajax call to a REST API service, and the elements of that list would fire ajax calls to that same service when clicked on that populated sub-lists below those elements, and so on to depth n.

The initial population of the list is easy: the controller makes the ajax call, gets the JSON object, assigns it to scope and the DOM is handled with a ng-repeat directive. I'm having trouble with the subsequent loading of the sub-lists.

In jQuery, I would have a function tied to each appropriately classed element clicked upon via onClick which would get the required parameters, take the JSON output, parse it into HTML and append that HTML after the element which fired the event. This is direct DOM manipulation and therefore Angular heresy.

I've already looked at this question here, but I still don't quite understand how to implement something like this "the Angular way".

Help?

Edit: Solved this problem by making a recursive directive. Instructions from here: http://jsfiddle.net/alalonde/NZum5/light/.

Code:

    var myApp = angular.module('myApp',[]);

    myApp.directive('uiTree', function() {
      return {
        template: '<ul class="uiTree"><ui-tree-node ng-repeat="node in tree"></ui-tree-node></ul>',
        replace: true,
        transclude: true,
        restrict: 'E',
        scope: {
          tree: '=ngModel',
          attrNodeId: "@",
          loadFn: '=',
          expandTo: '=',
          selectedId: '='
        },
        controller: function($scope, $element, $attrs) {
            $scope.loadFnName = $attrs.loadFn;
          // this seems like an egregious hack, but it is necessary for recursively-generated
          // trees to have access to the loader function
          if($scope.$parent.loadFn)
            $scope.loadFn = $scope.$parent.loadFn;

          // TODO expandTo shouldn't be two-way, currently we're copying it
          if($scope.expandTo && $scope.expandTo.length) {
            $scope.expansionNodes = angular.copy($scope.expandTo);
            var arrExpandTo = $scope.expansionNodes.split(",");
            $scope.nextExpandTo = arrExpandTo.shift();
            $scope.expansionNodes = arrExpandTo.join(",");
          }
        }
      };
    })
    .directive('uiTreeNode', ['$compile', '$timeout', function($compile, $timeout) {
      return { 
        restrict: 'E',
        replace: true,
        template: '<li>' + 
          '<div class="node" data-node-id="{{ nodeId() }}">' +
            '<a class="icon" ng-click="toggleNode(nodeId())""></a>' +
            '<a ng-hide="selectedId" ng-href="#/assets/{{ nodeId() }}">{{ node.name }}</a>' +
            '<span ng-show="selectedId" ng-class="css()" ng-click="setSelected(node)">' + 
                '{{ node.name }}</span>' +
          '</div>' +
        '</li>',
        link: function(scope, elm, attrs) {
          scope.nodeId = function(node) {
            var localNode = node || scope.node;
            return localNode[scope.attrNodeId];          
          };
          scope.toggleNode = function(nodeId) {
            var isVisible = elm.children(".uiTree:visible").length > 0;
            var childrenTree = elm.children(".uiTree");
            if(isVisible) {
              scope.$emit('nodeCollapsed', nodeId);
            } else if(nodeId) {
              scope.$emit('nodeExpanded', nodeId);
            }
            if(!isVisible && scope.loadFn && childrenTree.length === 0) {
              // load the children asynchronously
              var callback = function(arrChildren) {
                scope.node.children = arrChildren;
                scope.appendChildren();
                elm.find("a.icon i").show();
                elm.find("a.icon img").remove();
                scope.toggleNode(); // show it
              };
              var promiseOrNodes = scope.loadFn(nodeId, callback);
              if(promiseOrNodes && promiseOrNodes.then) {
                promiseOrNodes.then(callback);
              } else {
                  $timeout(function() {
                      callback(promiseOrNodes);
                  }, 0);
              }
              elm.find("a.icon i").hide();
              var imgUrl = "http://www.efsa.europa.eu/efsa_rep/repository/images/ajax-loader.gif";
              elm.find("a.icon").append('<img src="' + imgUrl + '" width="18" height="18">');
            } else {
              childrenTree.toggle(!isVisible);
              elm.find("a.icon i").toggleClass("icon-chevron-right");
              elm.find("a.icon i").toggleClass("icon-chevron-down");
            }
          };

          scope.appendChildren = function() {
            // Add children by $compiling and doing a new ui-tree directive
            // We need the load-fn attribute in there if it has been provided
            var childrenHtml = '<ui-tree ng-model="node.children" attr-node-id="' + 
                scope.attrNodeId + '"';
            if(scope.loadFn) {
              childrenHtml += ' load-fn="' + scope.loadFnName + '"';
            }
            // pass along all the variables
            if(scope.expansionNodes) {
              childrenHtml += ' expand-to="expansionNodes"';
            }
            if(scope.selectedId) {
              childrenHtml += ' selected-id="selectedId"';
            }
            childrenHtml += ' style="display: none"></ui-tree>';
            return elm.append($compile(childrenHtml)(scope));
          };

          scope.css = function() {
            return { 
              nodeLabel: true,
              selected: scope.selectedId && scope.nodeId() === scope.selectedId
            };
          };
          // emit an event up the scope.  Then, from the scope above this tree, a "selectNode"
          // event is expected to be broadcasted downwards to each node in the tree.
          // TODO this needs to be re-thought such that the controller doesn't need to manually
          // broadcast "selectNode" from outside of the directive scope.
          scope.setSelected = function(node) {
            scope.$emit("nodeSelected", node);
          };
          scope.$on("selectNode", function(event, node) {
            scope.selectedId = scope.nodeId(node);
          });

          if(scope.node.hasChildren) {
            elm.find("a.icon").append('<i class="icon-chevron-right"></i>');
          }

          if(scope.nextExpandTo && scope.nodeId() == parseInt(scope.nextExpandTo, 10)) {
            scope.toggleNode(scope.nodeId());
          }
        }
      };
    }]);

    function MyCtrl($scope, $timeout) {
        $scope.assets = [
            { assetId: 1, name: "parent 1", hasChildren: true},
            { assetId: 2, name: "parent 2", hasChildren: false}
        ];
        $scope.selected = {name: "child 111"};
        $scope.hierarchy = "1,11";
        $scope.loadChildren = function(nodeId) {
            return [
                {assetId: parseInt(nodeId + "1"), name: "child " + nodeId + "1", hasChildren: true}, 
                {assetId: parseInt(nodeId + "2"), name: "child " + nodeId + "2"}
            ];
        }
        $scope.$on("nodeSelected", function(event, node) {
            $scope.selected = node;
            $scope.$broadcast("selectNode", node);
        });
    }

Template:

    <div ng-controller="MyCtrl">
      <ui-tree ng-model="assets" load-fn="loadChildren" expand-to="hierarchy" selected-id="111" attr-node-id="assetId"></ui-tree>
        <div>selected: {{ selected.name }}</div> 
    </div>

1 Answer 1

1

Here's a solution which I prototyped for my own use.

https://embed.plnkr.co/PYVpWYrduDpLlsvto0wR/

Link updated

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.