Alex
Alex

Reputation: 6099

Nested ng-repeat gives error after 10 levels deep: 10 $digest() iterations reached

Getting this error: Error: [$rootScope:infdig] 10 $digest() iterations reached. Aborting!

I have created a reddit style nested comment system using AngularJS. But after 10 levels deep i get a very ugly error on my console that looks like this:

enter image description here

This happens after exactly 10 levels deep:

enter image description here

The nested comments are directives that look like this:

commentCont.tpl.html:

<div class="comment-cont">
    <div
        class="upvote-arrow"
        ng-show="!collapsed"
        ng-click="vote()"
        ng-class="{'active' : node.votedByMe}"
    >
        <div class="upvote-arrow-top"></div>
        <div class="upvote-arrow-bottom"></div>         
    </div>      
    <div class="wrapper">
        <div class="info">
            <span class="collapse" ng-click="collapsed = !collapsed">[ {{collapsed ? '+' : '–'}} ]</span>
            <span ng-class="{'collapsed-style' : collapsed}">
                <a ui-sref="main.profile({id: node.userId})">{{node.username}}</a>
                <span>{{node.upvotes}} point{{node.upvotes != 1 ? 's' : ''}}</span>
                <span mg-moment-auto-update="node.createdTime"></span>
                <span ng-show="collapsed">({{node.children.length}} child{{node.children.length != 1 ? 'ren' : ''}})</span>
            </span>
        </div>          
        <div class="text" ng-bind-html="node.comment | autolink | nl2br" ng-show="!collapsed"></div>
        <div class="reply" ng-show="!collapsed">
            <span ng-click="formHidden = !formHidden">reply</span>
        </div>          
    </div>
    <div class="input-area" ng-show="!collapsed">
        <form ng-show="!formHidden" name="form" autocomplete="off" novalidate ng-submit="submitted = true; submit(formData, form)">

            <div class="form-group comment-input-feedback-branch has-error" ng-show="form.comment.$invalid && form.comment.$dirty">
                <div ng-messages="form.comment.$error" ng-if="form.comment.$dirty">
                    <div class="input-feedback" ng-message="required">Comment text is required.</div>
                    <div class="input-feedback" ng-message="maxlength">Comment text cannot exceed 2500 characters.</div>
                </div>
            </div>

            <div
                class="form-group"
                ng-class="{ 'has-error' : form.comment.$invalid && form.comment.$dirty, 'has-success' : form.comment.$valid }"
            >
                <textarea
                    name="comment"
                    class="textarea comment-cont-textarea"
                    ng-model="formData.comment"
                    required
                    ng-maxlength="2500"
                    textarea-autoresize
                ></textarea>
            </div>
            <div class="form-group">
                <mg-button-loading
                    mgbl-condition="awaitingResponse"
                    mgbl-text="Save"
                    mgbl-loading-text="Saving..."
                    mgbl-class="btn-blue btn-small"
                    mgbl-disabled="!form.$valid"
                ></mg-button-loading>
                <mg-button-loading
                    mgbl-text="Cancel"
                    mgbl-class="btn-white btn-small"
                    ng-click="formHidden=true;"
                ></mg-button-loading>
            </div>                  
        </form>
    </div>
    <div ng-show="!collapsed">
        <div ng-repeat="node in node.children" ng-include="'commentTree'"></div>
    </div>
</div>

commentCont.directive.js:

(function () {
    'use strict';

    angular
        .module('app')
        .directive('commentCont', commentCont);

    /* @ngInject */
    function commentCont ($http, user, $timeout) {

        return {
            restrict: 'E',
            replace: true,
            scope: {
                node: '='
            },
            templateUrl: 'app/post/commentCont.tpl.html',
            link: function (scope, element, attrs) {

                var textarea = element.querySelector('.comment-cont-textarea');
                var voteOK = true;
                var action = '';

                var userInfo = user.get();

                scope.formHidden = true; // Do not ng-init="" inside an ng-repeat.
                scope.collapsed = false; // Do not ng-init="" inside an ng-repeat.

                // Autofocus textarea when reply link is clicked.
                scope.$watch('formHidden', function(newValue, oldValue) {
                    if (newValue !== true) {
                        $timeout(function() {
                            textarea[0].focus();
                        });                     
                    }
                });

                scope.submit = function (formData, form) {
                    if (form.$valid) {
                        scope.awaitingResponse = true;
                        formData.parentId = scope.node.id;
                        formData.postId = scope.node.postId;

                        formData.type = 4;
                        formData.fromUsername = userInfo.username;
                        formData.toId = scope.node.userId;
                        formData.fromImage = userInfo.thumbnail36x36.split('/img/')[1];

                        // console.log(formData);

                        $http.post('/api/comment', formData)
                            .then(function (response) {
                                scope.awaitingResponse = false;

                                if (response.data.success) {
                                    if (response.data.rateLimit) {
                                        alert(rateLimitMessage);
                                        return false;
                                    }
                                    // id and createdTime is sent with response.data.comment.
                                    var c = response.data.comment;
                                    var newCommentNode = {
                                        id: c.id,
                                        userId: userInfo.id,
                                        username: userInfo.username,
                                        parentId: formData.parentId,
                                        comment: formData.comment,
                                        upvotes: 0,
                                        createdTime: c.createdTime,
                                        votedByMe: false,
                                        children: [],
                                        postId: scope.node.postId
                                    };
                                    // console.log('user', user.get());
                                    // console.log('scope.node', scope.node);
                                    // console.log('response', response);
                                    // console.log('newCommentNode', newCommentNode);
                                    formData.comment = '';
                                    form.comment.$setPristine();
                                    scope.formHidden = true;
                                    scope.node.children.unshift(newCommentNode);                                    
                                }
                            });
                    }
                };

                scope.vote = function() {
                    if (voteOK) {
                        voteOK = false;
                        if (!scope.node.votedByMe) {
                            scope.node.votedByMe = true;
                            action = 'add';
                        } else {
                            scope.node.votedByMe = false;
                            action = 'remove';
                        }
                        var data = {
                            commentId: scope.node.id,
                            action: action
                        };
                        $http.post('/api/comment/vote', data)
                            .then(function (response) {
                                // console.log(response.data);
                                voteOK = true;
                                if (action === 'add') {
                                    scope.node.upvotes++;
                                } else {
                                    scope.node.upvotes--;
                                }
                            });                     
                    }
                };
            }
        };
    }

}());

The tree is being called like this:

<script type="text/ng-template" id="commentTree">
    <comment-cont
        node="node"
    ></comment-cont>
</script>
<div ng-repeat="node in tree[0].children" ng-include="'commentTree'"></div>

How can I have more than 10 levels of nested ng-repeat without getting an error like this?

Upvotes: 3

Views: 981

Answers (2)

Chen Koifman
Chen Koifman

Reputation: 1

I was dealing with a similar issue. end up with the following directive:

(function () {
'use strict';

angular
    .module('app')
    .directive('deferDom', ['$compile', ($compile) => {
        return {
            restrict: 'A',
            compile: (tElement) => {

                // Find, remove, and compile the li.node element.
                let $el = tElement.find( "li.node" );
                let transclude = $compile($el);
                $el.remove();

                return function($scope){
                    // Append li.node to the list.
                    $scope.$applyAsync(()=>{
                        tElement.append(transclude($scope));
                    });
                }

            },
        };
    }]);

})();

Upvotes: 0

Sasank Sunkavalli
Sasank Sunkavalli

Reputation: 3964

The default implementation of $digest() has limit of 10 iterations . If the scope is still dirty after 10 iterations error is thrown from $digest().

The below stated is one way of configuring the limit of digest iterations to 20.

var app = angular.module('plunker', [], function($rootScopeProvider) { 
  $rootScopeProvider.digestTtl(20); // 20 being the limit of iterations. 
});

But you should look in to stabilizing your model rather than configuring the limit of iterations.

Upvotes: 3

Related Questions