2

This is going to be a bit long winded, but here goes:

I am writing two separate web applications: one for generating a JSON object with a bunch of data and formatting information and another for turning that JSON object into a nice formatted HTML page for viewing. The particular issue I am having currently is with nested lists.

For the viewing application, I wrote a recursive function that will parse the supplied javascript object and create the HTML for a list with an arbitrary magnitude of nested elements.

function createUnorderedList(object) {
    var ul = $('<ol>');

    for (var i = 0; i < object.length; i++) {
        if (object[i].children === undefined) {
            ul.append($('<li>').text(object[i].value));
        } else {
            ul.append(
            $('<li>').html(
            object[i].value + "<ul>" + createUnorderedList(object[i].children) + "</ul>"));
        }
    }

    return ul.html();
}

The jsfiddle for that function can be seen here: jsfiddle: Recursive HTML List Generation

Now, I need to create a function for the JSON creation side of things. Having tried and failed to wrap my brain around how to do this recursively, I settled upon a more linear function, seen here:

function createList( array ) {
    var json = { "value": [] } ;
    var k = 0 ;
    if(array[array.length-1] === "") array.pop() ;
    for(var i=0; i<array.length; i++ ){
        json.value.push( { "value": array[i] } ) ;
        for(var j=i+1; j<array.length; j++) {
            if(array[j].charAt(0) === '\t') {
                array[j] = array[j].replace( /\t/, '' ) ;
                i++;
                if(json.value[k].children === undefined)
                    $.extend( json.value[k], { children : [] } ) ;
                json.value[k].children.push( { "value": array[j] } ) ;
            } else { k++; j=array.length; }
        }
    }
    return json ;
}

jsfiddle: JSON List Generation.

This function can only handle a nested list of one order of magnitude, which should suffice for now but is not ideal. How can I make this function capable of generating a json object with an arbitrary magnitude of nested elements?

Thank you.

2
  • I would make an array of tuples. Objects are not ordered. This may not give you the result you want. Commented Mar 12, 2015 at 16:51
  • Also, you can simplify your logic by using Array.prototype.reduce(). See makeOrderedList3() in my example. Commented Mar 12, 2015 at 18:30

2 Answers 2

3

Edit:: I misunderstood your question at first.

This is how you can parse a ul hierarchy into an object (view the JS console to see what is parsed out):

function itemsToHierarchy(node) {
    return $(node).find('> ul > li').toArray().map(function (item) {
        return {
            value: item.childNodes[0].textContent,
            children: itemsToHierarchy(item)
        };
    });
}

console.log(itemsToHierarchy($('#list')));
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.10.0/jquery.min.js"></script>
<!-- leaving the HTML scrunched up to save space -->
<div id="list"><ul><li>Item 1<ul><li>Item 1.1</li></ul></li><li>Item 2</li><li>Item 3<ul><li>Item 3.1</li><li>Item 3.2<ul><li>Item 3.2.1<ul><li>Item 3.2.1.1</li></ul></li></ul></li></ul></li><li>Item 4<ul><li>Item 4.1</li><li>Item 4.2</li></ul></li><li>Item 5<ul><li>Item 5.1</li></ul></li></ul></div>

At first I thought you were asking for help with your createList() function. Here is a recursive version that works for any nesting depth:

function getItems( array, depth ) {
    depth = depth || 0;
    var items = [],
        item = array.shift();

    while (item && countTabs(item) === depth) {
        items.push({
            value: item.replace(/^\t*/, ''),
            children: getItems(array, depth + 1)
        });
        item = array.shift();
    }

    if (item) { // not at this depth
        array.unshift(item);
    }

    return items;    
}

function countTabs(value) {
    return value.match(/^\t*/)[0].length;
}

The value returned from that can be used to build the HTML as follows:

function createUnorderedList(items) {
    return $('<ul>').append($.map(items, function (item) {
        var li = $('<li>').text(item.value);

        if (item.children.length) {
            li.append(createUnorderedList(item.children));
        }

        return li;
    }));        
}

Full solution: https://jsfiddle.net/1930t20f/

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

4 Comments

This solution looks like it should work (though I didn't include the "children" element if there are no children, but now I'm just being picky). Thank you for your help! Cheers :-)
@SimonPaonessa Yes, I know you were leaving children undefined when there were none and the above approach could be fairly easily modified to do so, but I think that having empty arrays of children is more consistent and allows for cleaner algorithms without any null checks.
Hahah. Maybe you're right. Like I said, not a big deal.
You can replace console.log(itemsToHierarchy($('#list'))); with $(document).append('<code>'+itemsToHierarchy($('#list'))+'</code>'); (or similar)
0

After some hard thinking I found a solution.

First I changed your code to create a <li> for every value. The children are the a <ul> after the <li>.

function createUnorderedList(object) {
    var ul = $('<ul>');
    for (var i = 0; i < object.length; i++) {
        ul.append($('<li>').text(object[i].value));
        if (object[i].children) {
            ul.append($('<ul>').html(createUnorderedList(object[i].children)));
        }
    }
    return ul.html();
}

Then I created a function that searches recursively all elements. The <ul> is the child of the <li> before it.

var createJSONRecursive = function(elem, arr) {
    var childNodes = elem.childNodes;
    var nodeTmp;
    for (var i = 0; i<childNodes.length; i++) {
        var node = {};
        var tagName = childNodes[i].tagName;
        if (tagName == 'LI') {
            node.value = childNodes[i].innerHTML;
            if (i < childNodes.length - 1 && childNodes[i+1].tagName == 'UL') {
                nodeTmp = node;
            }
            else {
                arr.push(node);
            }
        }
        else if (tagName == 'UL') {
            nodeTmp.children = [];
            createJSONRecursive(childNodes[i], nodeTmp.children);        
            arr.push(nodeTmp);
        }        
    }    
}

var createJSON = function(elem) {
    var json = {value:[]}
    createJSONRecursive(elem, json.value);
    return json;    
}

var json = createJSON(document.getElementById('div'));

Here is the fiddle: https://jsfiddle.net/8qc2mcyz/2/

2 Comments

Probably not a big deal, but this solution relies on having <ul>s directly inside other <ul>s, which is not technically valid HTML.
This isn't quite what I was trying to do, but it is certainly helpful. I was looking to generate the JSON object from user input in a textarea, where tabs would indicate hierarchy. The string that I am using in the second jsfiddle that I linked to would be (essentially) the input I'd be working with. You got me on the right track though, so thank you. Cheers. :-)

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.