1

SOLUTION

Thanks to Dave's elegeant solution and answer below here is the solution. Side note: fwiw the additional insight or homework like what Dave provided below is very valuable for noobs. Helps us stretch.

This code will walk an existing JSON tree is for example you wanted to parse each value for whatever reason. It doesn't build but walks. In my case I'm walking and parsing each comment to a richer class:

var db = [], instance = {}, commentCounter = [];

function hydrateComments(items, parent) {
     _.forEach(items, function(item) {
        _.has(item, 'descendants') ? hydrateComments(item.descendants, item) : 0;

        instance = new CommentModel(_.omit(item,['descendants']));
         // other parsers go here, example the counter for each level
         // parseCounter(comment);

        (parent['children'] = (parent['children'] || [])) && item.depth > 0 ?
            parent.children.push(instance) :
            parent.push(instance);
    });
}

hydrateComments(comments, storeComments);

ANGULAR DIRECTIVE

For those who use this code for building a tree I'm including a directive that can help you build a tree using the above mentioned tree.

Please note I've remove a lot of my own code and have not tested this, but I know I spent a ton of time trying to find both the tree and template so hopefully this helps you.

buildTree.$inject = [];
function buildTree() {
        link.$inject = ["scope", "elem", "attrs"];
        function link(scope, elem, attrs) {

        }

        CommentController.$inject = ["$scope"];
        function CommentController($scope) {

          $scope.$watchCollection(function () {
                return CommentDataService.getComments();
            },
            function (newComments, oldValue) {

                if (newComments) {
                    $scope.comments.model = newComments;
                }
            }, true);

        }

        return {
            restrict: "A",
            scope: {
               parent: "=cmoParent"
            },
            template: [
                "<div>",
                "<script type='text/ng-template'",
                    "id=" + '"' + "{[{ parent.app_id }]}" + '"' + " >",

                    "<div class='comment-post-column--content'>",
                        "<div cmo-comment-post",
                            "post-article=parent",
                            "post-comment=comment>",
                        "</div>",
                    "</div>",

                    "<ul ng-if='comment.children'>",
                        "<li class='comment-post-column--content'",
                            "ng-include=",
                            "'" + '"' + "{[{ parent.app_id }]}" + '"' + "'",
                            "ng-repeat='comment in comment.children",
                            "track by comment.app_id'>",
                        "</li>",
                    "</ul>",
                "</script>",


                "<ul class='conversation__timeline'>",
                    "<li class='conversation__post-container'",
                        "ng-include=",
                        "'" + '"' + "{[{ parent.app_id }]}" + '"' + "'",
                        "ng-repeat='comment in comments.model[parent.app_id]",
                        "track by comment.app_id'>",
                    "</li>",                
                    "<ul>",
                "</div>"

            ].join(' '),
            controller: CommentController,
            link: link
        }
    }

BONUS

I also discover a great trick. How to initialize and populate an array with one line of code. In my case I have a counter method that will count each comment at each level where I've used the tip:

parseCounter: function(comment) {
    var depth = comment.depth;
    (commentCounter[depth] = (commentCounter[depth] || [])) ? commentCounter[depth]++ : 0;
},

ORIGINAL QUESTION

The code below parses a multi-level array of objects with the purpose of parsing all objects to instances of “CommentModel”, which although simple in this example is much richer object class, but for brevity sake I’m simplified the object/class.

EXISTING STACK EXCHANGE CONTENT:

There is a ton of content on setting multi-dimensional arrays and almost all show the examples such as:

var item[‘level1’][‘level2’] = ‘value’;  

or

var item = [];
var item['level1'] = [];

or

var item = new Array([]) // and or objects

but, no examples of something like this:

var item[‘level1’].push(object)

QUESTIONS:

  1. Is there way to initialize a 2 level deep multi-dimensional array and at the same time push to it in one line of code?

    1.1 i.e. in my example below of parent[‘children’] I’m forced to check if it exists and if not set it. If I attempt parent[‘children’].push(instance) I obviously get a push on undefined exception. Is there a one liner or a better way to check if property exists and if not? I obviously cannot just set an empty array on parent on every iteration i.e. parent[‘children’] = []; and parent[‘children’] = value wont work

  2. Is it possible to move the initialize and validation to the CommentModel instance? I ask as I attempted to CommentModel.prototype['children'] = []; but then all child ('descendants') objects are added to every object in a proto property called “children”, which makes sense.

  3. side question - I think my tree iteration code function hydrateComments(items, parent) is concise but is there anything I can do to streamline further with lodash and/or angular? Most example I've seen tend to be verbose and don't really walk the branches.

PLUNKER & CODE

https://plnkr.co/edit/iXnezOplN4hNez14r5Tt?p=preview

    var comments = [
        {
            id: 1,
            depth: 0,
            subject: 'Subject one'
        },
        {
            id: 2,
            depth: 0,
            subject: 'Subject two',
            descendants: [
                {
                    id: 3,
                    depth: 1,
                    subject: 'Subject two dot one'
                },
                {
                    id: 4,
                    depth: 1,
                    subject: 'Subject two dot two'
                }
            ]
        },
        {
            id: 5,
            depth: 0,
            subject: 'Subject three',
            descendants: [
                {
                    id: 6,
                    depth: 1,
                    subject: 'Subject three dot one'
                },
                {
                    id: 7,
                    depth: 1,
                    subject: 'Subject three dot two',
                    descendants: [
                        {
                            id: 8,
                            depth: 2,
                            subject: 'Subject three dot two dot one'
                        },
                        {
                            id: 9,
                            depth: 2,
                            subject: 'Subject three dot two dot two'
                        }
                    ]
                }
            ]
        }
    ];

    function hydrateComments(items, parent) {
        _.forEach(items, function (item) {
            // create instance of CommentModel form comment. Simply example
            var instance = new CommentModel(item);

            // if we have descendants then injec the descendants array along with the
            // current comment object as we will use the instance as the "relative parent"
            if (_.has(instance, 'descendants')) {
                hydrateComments(instance.descendants, instance);
            }

            // we check is parent has a property of children, if not, we set it
            // NOTE : 3 lines of code ? is there a more concise approach
            if (!_.has(parent, 'children')) {
                parent['children'] = [];
            }

            // if depth id greater than 0, we push all instances of CommentModel of that depth to the
            // parent object property 'children'. If depth is 0, we push to root of array
            if (item.depth > 0) {
                parent.children.push(instance);
            } else {
                parent.push(instance);
            }
        })
    }

    // simple example, but lets assume much richer class / object
    function CommentModel(comment) {
        this.id = comment.id;
        this.depth = comment.depth;
        this.subject = comment.subject;
        this.descendants = comment.descendants;
    }

    var output = [];
    // init - pass in data and the root array i.e. output
    hydrateComments(comments, output);

    // Tada - a hydrated multi-level array
    console.log('Iteration output for comments  : ', output)

1 Answer 1

1

To initialise array in single statement you can do as follows

Method 1: (To initialise parent['children']) ANS to Q#1

Plunker for #1: https://plnkr.co/edit/lmkq8mUWaVrclUY2CoMt?p=preview

function hydrateComments(items, parent) {
  _.forEach(items, function(item) {
    // create instance of CommentModel form comment. Simply example 
    var instance = new CommentModel(item);

    // if we have descendants then injec the descendants array along with the 
    // current comment object as we will use the instance as the "relative parent"
    _.has(instance, 'descendants') ? hydrateComments(instance.descendants, instance) : 0;

    //Less eff. and less readable then method #2
    (parent['children'] = (parent['children'] || [])) && item.depth > 0 ?
      parent.children.push(instance) :
      parent.push(instance);


  });
}

Method 2: (To initialise parent['children']) ANS to Q#2 -- I'd prefer this.

Plunker for #2: https://plnkr.co/edit/zBsF5o9JMb6ETHKOv8eE?p=preview

function CommentModel(comment) {
  this.id = comment.id;
  this.depth = comment.depth;
  this.subject = comment.subject;
  this.descendants = comment.descendants;
  //Initialise children in constructer itself! :)
  this.children = [];
}

function hydrateComments(items, parent) {
  _.forEach(items, function(item) {
    // create instance of CommentModel form comment. Simply example 
    var instance = new CommentModel(item);

    // if we have descendants then injec the descendants array along with the 
    // current comment object as we will use the instance as the "relative parent"
    _.has(instance, 'descendants') ? hydrateComments(instance.descendants, instance) : 0;

    item.depth > 0 ? parent.children.push(instance) : parent.push(instance);

  });
}

ANS to Q#3

I feel your code is ok. But if depth increases too much, you might encounter stackoverflow. To get rid of this issue with recursion using trampolines. But if you are sure depth is not

I'd like to quote few lines from above article:

What this graph doesn’t show is that after 30,000 recursive invocations the browser hung; to the point it had to be forcibly shut down. Meanwhile the trampoline continued bouncing through hundreds of thousands of invocations. There are no practical limits to the number of bounces a trampoline can make.

But only use trampoline is you know that the depth is deep enough to cause stack overflow.

Hope this helps !

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

2 Comments

Dave, thanks a million. The additional insight is often not provided by very much appreciated by a noob like me.
@Nolan you are most welcome! Keep up the good work and be awesome!

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.