quinton
quinton

Reputation: 439

How can I recursively create a UL/LI's from JSON data - multiple layers deep

I am trying to use use the following JSON data to create the following similar structure in a recursive inner function with not much luck, really need some help and so if anyone can assist please do. Thank you in advance.

<ul>
    <li></li>
    <li>
        <a href=""></a>
        <div>
            <ul>
                <li>
                    <a href=""></a>
                    <div>
                         ....etc
                    </div>
                </li>
            </ul>
        </div>
    </li>
</ul>

the JSON data I am using is as follows:

    var JSON = {
    menu: [
        {id: '0',sub: [
            {name: 'lorem ipsum 0-0',link: '0-0', sub: null},
            {name: 'lorem ipsum 0-1',link: '0-1', sub: null},
            {name: 'lorem ipsum 0-2',link: '0-2', sub: null}
            ]
        },
        {id: '1',sub: null},
        {id: '2',sub: [
            {name: 'lorem ipsum 2-0',link: '2-0', sub: null},
            {name: 'lorem ipsum 2-1',link: '2-1', sub: null},
            {name: 'lorem ipsum 2-2',link: '2-2', sub: [
                {name: 'lorem ipsum 2-2-0',link: '2-2-0', sub: null},
                {name: 'lorem ipsum 2-2-1',link: '2-2-1', sub: null},
                {name: 'lorem ipsum 2-2-2',link: '2-2-2', sub: null},
                {name: 'lorem ipsum 2-2-3',link: '2-2-3', sub: null},
                {name: 'lorem ipsum 2-2-4',link: '2-2-4', sub: null},
                {name: 'lorem ipsum 2-2-5',link: '2-2-5', sub: null},
                {name: 'lorem ipsum 2-2-6',link: '2-2-6', sub: null}
            ]},
            {name: 'lorem ipsum 2-3',link: '2-3', sub: null},
            {name: 'lorem ipsum 2-4',link: '2-4', sub: null},
            {name: 'lorem ipsum 2-5',link: '2-5', sub: null}
            ]
        },
        {id: '3',sub: null}
    ]
}

and the code I have created (incomplete, this is the brain teaser I need help on) is:

$(function(){

    $.fn.dropdown = function(settings){
        var that = this;
        var settings = $.extend({}, $.fn.dropdown.defaults, settings);
        var methods = {
            isArray: function(o){
                return Object.prototype.toString.call(o) === '[object Array]';
            },
            createDropdownCode: function(arr){
                var menu = arr.menu;
                var html = null;
                var menusort = function(menu){
                    html = that;

                    that.find("li").each(function(idx){

                        var menuList = menu[idx].sub;
                        var baseContainer = $(this);
                        var count = -1;

                        var subsort = (function(){

                            count += 1;

                            return function(submenu, pb){

                                var subblock;
                                subblock = $("<div />").append('<ul />');

                                if(methods.isArray(submenu)){

                                    for(var i=0;i<submenu.length;i++){

                                        var l = $("<li />").append("<a href='"+ submenu[i].link +"'>"+ submenu[i].name +"</a>");

                                        subblock.find('ul').append(l);

                                        if(pb !== undefined && i == submenu.length-1){
                                            pb.append(subblock)
                                        }

                                        if(methods.isArray(submenu[i].sub)){
                                            subsort(submenu[i].sub, subblock.find('ul li').eq(i));
                                        }

                                    }
                                }
                            }
                        })()
                        subsort(menuList)
                    })
                }
                menusort(menu);
                return null; //html !== null ? html.html() : null;
            },
            init: function(){

                // filter through json
                // create the div=>ul=>li
                if(settings.jsonData === undefined || settings.jsonData === null){
                    console.warn('No JSON Data passed')
                    return;
                }else{
                    if(!methods.isArray(settings.jsonData.menu)){
                        console.warn('No JSON Data passed')
                        return; // error, no data!
                    }
                }


                //var html = methods.createBlock(settings.jsonData.menu[0].sub);
                var html = methods.createDropdownCode(settings.jsonData);
                //console.log(html) 
            }
        }

        methods.init();

        return that;

    }

    $.fn.dropdown.defaults = {
        jsonData: null
    }

})

$('#menu').dropdown({
    jsonData: JSON
});

integrated code used, thanks to the individual that gave a close enough answer - Although will study the others.

$.fn.dropdown = function(settings){

    var that = this;
    var settings = $.extend({}, $.fn.dropdown.defaults, settings);

    var methods = {
        createDropDownCode: function(arr){

            // loop through li's of primary menu
            that.find("li").each(function(idx){

                $(this).append( menusort(arr.menu[idx].sub) );

                function menusort(data){
                    if(data !== null)   
                        var html = "<div><ul>";

                    for(item in data){
                        html += "<li>";
                        if(typeof(data[item].sub) === 'object'){
                            html += "<a href='" + data[item].link + "'>" + data[item].name + "</a>";
                            if($.isArray(data[item].sub))
                                html += menusort(data[item].sub);
                        }
                        html += "</li>"
                    }
                    if(data !== null)
                        html += "</ul></div>";
                    return html;
                }
            })
        },
        init: function(){
            var html = methods.createDropDownCode(settings.jsonData);

        }
    }

    methods.init();

}

Upvotes: 8

Views: 25447

Answers (8)

Scott Sauyet
Scott Sauyet

Reputation: 50797

An edit to an old answer brought this question back up, and I think in modern JS this becomes easier than many answers above.

Here is one solution, using template strings and parameter destructuring:

const buildMenu = (nodes) =>
  `<ul>${nodes.map(
    ({id, name, link, sub}) => sub
      ? `<li>${name ? `<a href="${link}">${name}</a>` : id}<div>${buildMenu(sub)}</div></li>`
      : `<li>${id || `<a href="${link}">${name}</a>`}</li>`
  ).join('')}</ul>`

var JSON = {menu: [{id: '0',sub: [{name: 'lorem ipsum 0-0',link: '0-0', sub: null}, {name: 'lorem ipsum 0-1',link: '0-1', sub: null}, {name: 'lorem ipsum 0-2',link: '0-2', sub: null}]}, {id: '1',sub: null}, {id: '2',sub: [{name: 'lorem ipsum 2-0',link: '2-0', sub: null}, {name: 'lorem ipsum 2-1',link: '2-1', sub: null}, {name: 'lorem ipsum 2-2',link: '2-2', sub: [{name: 'lorem ipsum 2-2-0',link: '2-2-0', sub: null}, {name: 'lorem ipsum 2-2-1',link: '2-2-1', sub: null}, {name: 'lorem ipsum 2-2-2',link: '2-2-2', sub: null}, {name: 'lorem ipsum 2-2-3',link: '2-2-3', sub: null}, {name: 'lorem ipsum 2-2-4',link: '2-2-4', sub: null}, {name: 'lorem ipsum 2-2-5',link: '2-2-5', sub: null}, {name: 'lorem ipsum 2-2-6',link: '2-2-6', sub: null}]}, {name: 'lorem ipsum 2-3',link: '2-3', sub: null}, {name: 'lorem ipsum 2-4',link: '2-4', sub: null}, {name: 'lorem ipsum 2-5',link: '2-5', sub: null}]}, {id: '3',sub: null}]}

const tree = buildMenu(JSON.menu);

document.getElementById('output').innerHTML = tree
<div id="output"></div>

Most of the complexity here is simply in handling different formats for nodes that have sub-arrays and those that don't and for those with name/link properties and those with just an id property. If the input was a little more consistent, this would be simpler still. Nonetheless, this is not a horrible version. If you like to work with string-based DOM solutions, it's fairly clean.

Note that while modern features make the solution cleaner, we could do the same thing in ES5:

const buildMenu = (nodes) =>
  '<ul>' + nodes.map (
    (node) => node.sub
      ? '<li>' + (node.name ? '<a href="' + node.link + '">' + node.name + '</a>' : node.id) + '<div>' + buildMenu(node.sub) + '</div></li>'
      : '<li>' + (node.id || '<a href="' + node.link + '">' + node.name + '</a>') + '</li>'
  ) .join ('') + '</ul>'

Upvotes: 0

Pure ES6

var foo=(arg)=>
`<ul>
${arg.map(elem=>
    elem.sub?
        `<li>${foo(elem.sub)}</li>`
        :`<li>${elem.name}</li>`
    ).join('')}
</ul>`

JSON example

   var bar = [
  {
    name: 'Home'
  }, {
    name: 'About'
  }, {
    name: 'Portfolio'
  }, {
    name: 'Blog'
  }, {
    name: 'Contacts'
  }, {
    name: 'Features',
    sub: [
      {
        name: 'Multipage'
      }, {
        name: 'Options',
        sub: [
          {
            name: 'General'
          }, {
            name: 'Sidebars'
          }, {
            name: 'Fonts'
          }, {
            name: 'Socials'
          }
        ]
      }, {
        name: 'Page'
      }, {
        name: 'FAQ'
      }
    ]
  }
]
var result=foo(bar)

Your 'result' will be valid HTML

Upvotes: 3

HumbleGeek
HumbleGeek

Reputation: 51

This is like a complete solution for generating UL/LI recursively from JSON config, which has customizable classes for each node and support of expand and collapse events for each node. This provides just a basic working model, from which you ll be able to expand and customize to your needs.

I found this answer from https://techmeals.com/fe/questions/javascript/6/How-can-I-create-a-dynamic-tree-of-UL-and-LI-from-JSON-config

Example JSON config file:

var config = {
    "Menu-1-Level-1": {
        "label": "Menu-1-Level-1",
        "type": "treeView",
        "class": "Menu-1-Level-1",
        "children": [
            {
                label: "Menu-1-Level-2",
                type: "treeView",
                "class": "Menu-1-Level-2",
                children: [
                    {
                        label: "Menu-1-Level-3",
                        class: "Menu-1-Level-3"
                    }
                ]
            },
            {
                label : "Menu-2-Level-2",
                class: "Menu-2-Level-2"
            }
        ]
    },
    "Menu-2-Level-1": {
        "label": "Menu-2-Level-1",
        "type": "treeView",
        "class": "Menu-2-Level-1",
        "children": [
            {
                label: "Menu-1-Level-2",
                class: "Menu-1-Level-2",
                type: "treeView",
                children: [
                    {
                        label: "Menu-1-Level-3",
                        class: "Menu-1-Level-3"
                    }
                ]
            },
            {
                label : "Menu-2-Level-2",
                class : "Menu-2-Level-2"
            }
        ]
    }
};

HTML Code:

<!DOCTYPE html>
<html>

<head>
    <title>Tree Menu</title>
    <script src="http://code.jquery.com/jquery-1.11.2.min.js" type="text/javascript"></script>
    <script src="tree.js" type="text/javascript"></script>
    <link href="tree.css" rel="stylesheet">
</head>

<body>
    <div class="treeContainer">
        <div class="tree"></div>
    </div>
    <script src="testPage.js" type="text/javascript"></script>
</body>

</html>

Tree.js

var tree;

tree = function (treeNodeParent, dataObj) {
    this.dataObj = dataObj;
    this.treeNodeParent = treeNodeParent;
    this.treeNode = $(document.createElement("ul")).addClass("treeNode");
};

tree.prototype.expandCollapse = function (e) {
    var target = $(e.currentTarget), parentLabel = target.parent();

    if (parentLabel.hasClass("collapsed")) {
        parentLabel.removeClass("collapsed").addClass("expanded");
    } else {
        parentLabel.addClass("collapsed").removeClass("expanded");
    }
};

tree.prototype.attachEvents = function () {
    var me = this;
    me.treeNodeParent.delegate(".collapsed label, .expanded label", "click", me.expandCollapse);
};

tree.prototype.attachMarkUp = function () {
    var me = this;
    me.treeNodeParent.append(me.treeNode);
};

tree.prototype.getEachNodeMarkup = function (nodeObj, rootNode, selector) {
    var selectedNode, i, me = this;

    if (nodeObj.children) {

        if (!selector) {
            selectedNode = rootNode;
        } else {
            selectedNode = rootNode.find(selector);
        }

        nodeObj.class = nodeObj.class ? nodeObj.class : "";
        selectedNode.append($.parseHTML("<li name=" + nodeObj.label + " class='collapsed " + nodeObj.class + "'>" + "<label>" + nodeObj.label + "</label>" + "<ul></ul></li>"));
        selector = selector + " li[name=" + nodeObj.label + "] > ul";

        for (i = 0; i < nodeObj.children.length; i = i + 1) {
            me.getEachNodeMarkup(nodeObj.children[i], rootNode, selector);
        }

    } else {
        nodeObj.class = nodeObj.class ? nodeObj.class : "";
        rootNode.find(selector).append($.parseHTML("<li name=" + nodeObj.label + " class='" + nodeObj.class + "'>" + "<label>" + nodeObj.label + "</label>" + "</li>"));
    }
};

tree.prototype.getTree = function () {
    var component, me = this;

    for (component in me.dataObj) {
        if (me.dataObj.hasOwnProperty(component)) {
            me.getEachNodeMarkup(me.dataObj[component], me.treeNode, "");
        }
    }
    me.attachMarkUp();
    me.attachEvents();
    return me.treeNode;
};

Tree.css

.treeNode .collapsed > ul, .collapsed > li {
    display: none;
}

.treeNode .expanded > ul, .expanded > li {
    display: block;
}

testPage.js

// the variable "config" is nothing but the config JSON defined initially. 
treeNode = new tree($('.treeContainer .tree'), config); 
treeNodeObj = treeNode.getTree();

Look at the example provided at https://jsfiddle.net/3s3k3zLL/

Upvotes: 1

talsibony
talsibony

Reputation: 8756

I was searching for general parent child element function and I saw these answers, and I took some pieces of code from here and there and made this function. I decided to share my code as an answer, in case someone like me will find this post when he is searching for a general parent child html element draw function:

function drawRecElements(arr, html, elements) {
    if (typeof (html) === 'undefined') {
        var html = '';
    }
    if (typeof (elements) === 'undefined') {
        var elements = {child: '<li>', childClose: '</li>', parent: '<ul>', parentClose: '</ul>'};
    }
    if (typeof (arr) === 'string') {
        return elements.child + arr + elements.childClose;
    } else if (typeof (arr) === 'object') {
        for (i in arr) {
            if (typeof (arr[i]) === 'string') {
                html += elements.parent + elements.child + i + elements.childClose + elements.child + arr[i] + elements.childClose + elements.parentClose;
            } else if(typeof (i) === 'string' && (isNaN(i))){
                html += elements.parent + elements.child + i + elements.childClose + elements.child + drawRecElements(arr[i],'',elements) + elements.childClose + elements.parentClose;
            } else if (typeof (arr[i]) === 'object') {
               html = drawRecElements(arr[i], html,elements);
            }
        }
    }
    return html;
}

https://jsfiddle.net/kxn442z5/1/

Upvotes: 1

Nadeemmnn Mohd
Nadeemmnn Mohd

Reputation: 724

Fiddle

Code:

JS

var jsonstring = [{
  "id": '1',
  "children": [{
    "id": '2'
  }, {
    "id": '3',
    "children": [{
      "id": '4'
    }]
  }]
}, {
  "id": '5'
}];

 var htmlStr= recurse( jsonstring );
$('#test').append(htmlStr);


function recurse( data ) {
  var htmlRetStr = "<ul>"; 
  for (var key in data) { 
        if (typeof(data[key])== 'object' && data[key] != null) {
        var x=key*1;        
        if(isNaN(x)){
            htmlRetStr += "<li>" + key + ":<ul>";
            }
            htmlRetStr += recurse( data[key] );
            htmlRetStr += '</ul></li>';
        } else {
            htmlRetStr += ("<li>" + key + ': &quot;' + data[key] + '&quot;</li  >' );
        }
  };
  htmlRetStr += '</ul >';    
  return( htmlRetStr );
}

HtML

<div id="test"></div>

CSS

li ul ul li {
    padding-left: 10px;
}

li ul ul ul {
    padding: 0px;
}

Upvotes: 1

Mr. Polywhirl
Mr. Polywhirl

Reputation: 48630

This solution uses a single recursive function. I simplified logic by using Array's map() prototype function.

$(function () {
    $("body").html(makeUnorderedList(getData().menu));
});

function makeUnorderedList(data, li) {
    return $('<ul>').append(data.map(function (el) {
        var li = li || $('<li>');
        if (el.id || el.link) li.append($('<a>', {
            text : el.id || el.link,
            href : '#' + (el.id || el.link),
            name : el.name
        }));
        if (el.sub) li.append(makeUnorderedList(el.sub, li));
        return li;
    }));
}

function getData() {
    return {
        menu: [{
            id: '0',
            sub: [{
                name: 'lorem ipsum 0-0',
                link: '0-0',
                sub: null
            }, {
                name: 'lorem ipsum 0-1',
                link: '0-1',
                sub: null
            }, {
                name: 'lorem ipsum 0-2',
                link: '0-2',
                sub: null
            }]
        }, {
            id: '1',
            sub: null
        }, {
            id: '2',
            sub: [{
                name: 'lorem ipsum 2-0',
                link: '2-0',
                sub: null
            }, {
                name: 'lorem ipsum 2-1',
                link: '2-1',
                sub: null
            }, {
                name: 'lorem ipsum 2-2',
                link: '2-2',
                sub: [{
                    name: 'lorem ipsum 2-2-0',
                    link: '2-2-0',
                    sub: null
                }, {
                    name: 'lorem ipsum 2-2-1',
                    link: '2-2-1',
                    sub: null
                }, {
                    name: 'lorem ipsum 2-2-2',
                    link: '2-2-2',
                    sub: null
                }, {
                    name: 'lorem ipsum 2-2-3',
                    link: '2-2-3',
                    sub: null
                }, {
                    name: 'lorem ipsum 2-2-4',
                    link: '2-2-4',
                    sub: null
                }, {
                    name: 'lorem ipsum 2-2-5',
                    link: '2-2-5',
                    sub: null
                }, {
                    name: 'lorem ipsum 2-2-6',
                    link: '2-2-6',
                    sub: null
                }]
            }, {
                name: 'lorem ipsum 2-3',
                link: '2-3',
                sub: null
            }, {
                name: 'lorem ipsum 2-4',
                link: '2-4',
                sub: null
            }, {
                name: 'lorem ipsum 2-5',
                link: '2-5',
                sub: null
            }]
        }, {
            id: '3',
            sub: null
        }]
    };
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>


Extension

Here is a more dynamic approach. You get to choose how your list items are rendered and what the child property is. The mapFunc paramater is a callback that gives you access to the current child node and its parent.

The scope of the mapFunc is the item. So you could use item as well as this to refer to said item.

$(function () {
    $("body").html(makeUnorderedList(getData().menu, function(item, index, parent) {
      // `item` and `this` are the same.
      return $('<a>', {
            text : (item.id || item.link),
            href : '#' + (item.id || item.link),
            name : item.name,
            'data-index' : index
        });
    }, 'sub'));
});

function makeUnorderedList(data, mapFunc, childProp, li, parent) {
    return $('<ul>').append(data.map(function (el, index) {
        var li = li || $('<li>');
        li.append(mapFunc.call(el, el, index, parent));
        if (el[childProp]) {
            li.append(makeUnorderedList(el[childProp], mapFunc, childProp, li, data));
        }
        return li;
    }));
}

Upvotes: 2

R&#233;mi Breton
R&#233;mi Breton

Reputation: 4259

You can try this recursive function I've just coded:

function buildList(data, isSub){
    var html = (isSub)?'<div>':''; // Wrap with div if true
    html += '<ul>';
    for(item in data){
        html += '<li>';
        if(typeof(data[item].sub) === 'object'){ // An array will return 'object'
            if(isSub){
                html += '<a href="' + data[item].link + '">' + data[item].name + '</a>';
            } else {
                html += data[item].id; // Submenu found, but top level list item.
            }
            html += buildList(data[item].sub, true); // Submenu found. Calling recursively same method (and wrapping it in a div)
        } else {
            html += data[item].id // No submenu
        }
        html += '</li>';
    }
    html += '</ul>';
    html += (isSub)?'</div>':'';
    return html;
}

It returns the html for the menu, so use it like that: var html = buildList(JSON.menu, false);

I believe it is faster because it's in pure JavaScript, and it doesn't create text nodes or DOM elements for every iteration. Just call .innerHTML or $('...').html() at the end when you're done instead of adding HTML immediately for every menu.

JSFiddled: http://jsfiddle.net/remibreton/csQL8/

Upvotes: 17

georg
georg

Reputation: 214959

Make two functions makeUL and makeLI. makeUL calls makeLI on each element, and makeLI calls makeUL if there's sub elements:

function makeUL(lst) {
    ...
    $(lst).each(function() { html.push(makeLI(this)) });
    ...
    return html.join("\n");
}

function makeLI(elem) {
    ...
    if (elem.sub)
        html.push('<div>' + makeUL(elem.sub) + '</div>');
    ...
    return html.join("\n");
}

http://jsfiddle.net/BvDW3/

Needs to be adapted to your needs, but you got the idea.

Upvotes: 6

Related Questions