Search code examples
angularjsangularjs-scopeangular-directiveangularjs-digest

ng-repeat within directive will not update when adding or modifying objects to the existing collection


I've got a treeview which contains objects with nested arrays that I am trying to populate.

A working plunker may be found here.

Here is a breakdown of how the code is structured and what I have tried thus far.

Within my treeview.html view I am loading the treeview directive I have created: I currently have two just to test the bindings. The second one will not populate until the incident checkbox has been checked.

<H3>Tree View Sameple</H3>

<div>
  <h3>Treeview 1</h3>
    <tree src="tree.treeData" iobj="object" filter="tree.getAggregateInfo(object, isSelected)"></tree>
    <h3>Treeview 2 once object has been updated</h3>
    <tree src="tree.treeData2" iobj="object" filter="tree.getAggregateInfo(object, isSelected)"></tree>
</div>
<hr/>
<p>javascript object data</p>
{{tree.treeData}}

The bottom binding is just showing the output of the js file. Wihtin the treeview when you click on the incident checkbox I am simulating a new nested collection being retrieved that should be inserted as a child element under the incident node of the tree.

Here is the code used in the treeviewController.js

(function () {

    angular.module('app')
           .controller('treeviewController', ['$log', '$scope', '$timeout', treeviewController]);
    function treeviewController($log, $scope, $timeout) {

        var vm = this;
       //storage container for last node selected in tree
        vm.lastNodeSelected = '';

        vm.treeData2 = {};//test container to reflect object once updated
        //original data not an arry but an object holding an array
        vm.treeData = 
            {
                "children": [
                  {
                      "name": "Document Type",
                      "children": [
                        {
                            "name": "Incident",
                            "documentType": "Document Type",
                            "aggregateUponField": "_ocommon.documenttype",
                            "parent": "",
                            "count": 2950,
                            "children": null
                        },
                        {
                            "name": "Some Event",
                            "documentType": "Document Type",
                            "aggregateUponField": "_ocommon.documenttype",
                            "parent": "",
                            "count": 2736,
                            "children": [{
                                        "name": "Some Event Date",
                                        "children": [
                                                    {
                                                    "name": "2008",
                                                    "documentType": "Incident Date",
                                                    "aggregateUponField": "dateincidentstart",
                                                    "parent": "",
                                                    "count": 451,
                                                    "children": null
                                                    },
                                                    {
                                                    "name": "2009",
                                                    "documentType": "Incident Date",
                                                    "aggregateUponField": "dateincidentstart",
                                                    "parent": "",
                                                    "count": 407,
                                                   "children": null
                                                   },
                                                   {
                                                   "name": "2010",
                                                   "documentType": "Incident Date",
                                                   "aggregateUponField": "dateincidentstart",
                                                   "parent": "",
                                                   "count": 426,
                                                   "children": null
                                                   }
                                                  ]
                                       }]
                        }
                      ]
                  }
                ]
            };

        //aggregate data to be nested under incident
        vm.newChildData = [
              {
                  "name": "Incident Date",
                  "children": [
                    {
                        "name": "2008",
                        "documentType": "Incident Date",
                        "aggregateUponField": "dateincidentstart",
                        "parent": "",
                        "count": 451,
                        "children": null
                    },
                    {
                        "name": "2009",
                        "documentType": "Incident Date",
                        "aggregateUponField": "dateincidentstart",
                        "parent": "",
                        "count": 407,
                        "children": null
                    },
                    {
                        "name": "2010",
                        "documentType": "Incident Date",
                        "aggregateUponField": "dateincidentstart",
                        "parent": "",
                        "count": 426,
                        "children": null
                    }
                  ]
              } 

        ];


        var loadData = function () {
            //stip out array
            var children = vm.treeData.children;
            //search array for matching parent
            $log.info(vm.lastNodeSelected);
            var cnt = children.length;
            for (var i = 0; i < cnt; i++) {
                if (children[i].children) {
                    var innerCnt = children[i].children.length;
                    for (var c = 0; c < innerCnt; c++) {
                        if (children[i].children[c].name === vm.lastNodeSelected) {
                            children[i].children[c].children = vm.newChildData;

                        }
                    }
                }
            }

            //wrap back in object and assign to ngModel
            vm.updatedList = { children };
            // $log.info(updatedList);      

            vm.treeData2 = vm.updatedList;
            $timeout(function() {
                $scope.$apply(function() {
                    vm.treeData = vm.updatedList;
                })} , 0);
        }

        vm.getAggregateInfo = function (node) {

            vm.lastNodeSelected = node.name;
            loadData();
           // $log.info(vm.lastNodeSelected);
        };


    }

})();

Following is the treeview directive.

angular.module('app')
    .directive('tree', function () {
        return {
            restrict: 'E',
            replace: true,
            scope: {
                t: '=src',
                filter: '&'
            },
            template: '<ul><branch ng-repeat="c in t.children track by $index"  src="c" filter="filter({ object: object, isSelected: isSelected })"></branch></ul>'

        };
    });

angular.module('app')
.directive('branch', function ($compile) {

    return {
        restrict: 'E',
        replace: true,
        scope: {
            b: '=src',
            filter: '&',
            checked: '=ngModel'
        },

        template: '<li class="dir-tree"><input type="checkbox" ng-click="innerCall()" ng-model="b.$$hashKey" ng-change="stateChanged(b.$$hashKey)"  />{{ b.name }}  <span ng-hide="visible"> ({{ b.count }})</span></li>',
        link: function (scope, element, attrs) {

            var clicked = '';
            var hasChildren = angular.isArray(scope.b.children);
            scope.visible = hasChildren;
            if (hasChildren) {
                element.append('<tree src="b" filter="filter({ object: object, isSelected: isSelected })"></tree>');
                $compile(element.contents())(scope);
            }
            element.on('click', function (event) {
                event.stopPropagation();
                if (hasChildren) {
                    element.toggleClass('collapsed');
                }
            });
            scope.stateChanged = function (b) {
                clicked = b;
            };

            scope.innerCall = function () {
                scope.filter({ object: scope.b, isSelected: clicked });
            };
        }
    };
});

What I am seeing is when I click on the incident check box the javascript object does update. You can see this in the output as well as the second treeview that gets bound once the check box is selected.

I suspect the issue is related to the $digest cycle of the event and the tree is not being notified to actually update. I've tried as you can see in the controller using a $scope.$apply() wrapped in a timeout but this has not worked to update the original tree.

Doing additive actions like a .push() to the array updates however trying to insert a new object into an existing array does not seem to work.

Any suggestions on how to get the tree to update and reflect the child objects?


Solution

  • Here's a slightly modified Plunker to highlight (and partially fix) the problem.

    I added the following lines to the link function in the branchdirective:

    scope.$watch('b', function (newvalue, oldvalue) {
        if (!oldvalue.children && newvalue.children) {
            scope.visible = true;
            element.append('<tree src="b" filter="filter({ object: object, isSelected: isSelected })"></tree>');
            $compile(element.contents())(scope);
         }
    }, true);
    

    The linkfunction is not called again, when the tree structure gets updated. Thus, if there are new children to be added, those will not get compiled for Angular.