Create a nested UL menu based on the URL path structure of menu items
Asked Answered
A

6

8

I have an array of menu items, each containing Name and URL like this:

var menuItems = [  
    {  
        name : "Store",  
        url : "/store"  
    },  
    {  
        name : "Travel",  
        url : "/store/travel"  
    },  
    {  
        name : "Gardening",  
        url : "/store/gardening"  
    },  
    {  
        name : "Healthy Eating",  
        url : "/store/healthy-eating"  
    },  
    {  
        name : "Cook Books",  
        url : "/store/healthy-eating/cook-books"  
    },  
    {  
        name : "Single Meal Gifts",  
        url : "/store/healthy-eating/single-meal-gifts"  
    },  
    {  
        name : "Outdoor Recreation",  
        url : "/store/outdoor-recreation"  
    },  
    {  
        name : "Hiking",  
        url : "/store/outdoor-recreation/hiking"  
    },  
    {  
        name : "Snowshoeing",  
        url : "/store/outdoor-recreation/hiking/snowshoeing"  
    },  
    {  
        name : "Skiing",  
        url : "/store/outdoor-recreation/skiing"  
    },  
    {  
        name : "Physical Fitness",  
        url : "/store/physical-fitness"  
    },  
    {  
        name : "Provident Living",  
        url : "/store/provident-living"  
    }  
]  

I've been trying with no success to render this as an unordered list with a nested UL structure that follows the URL path structure like so:

<ul>  
    <li><a href="/store">Store</a>  
        <ul>  
        <li><a href="/store/travel">Travel</a></li>  
        <li><a href="/store/gardening">Gardening</a></li>  
        <li><a href="/store/healthy-eating">Healthy Eating</a>  
            <ul>  
            <li><a href="/store/healthy-eating/cook-books">Cook Books</a></li>  
            <li><a href="/store/healthy-eating/single-meal-gifts">Single Meal Gifts</a></li>
            </ul>  
        </li>
        <li><a href="/store/outdoor-recreation">Outdoor Recreation</a>  
            <ul>  
            <li><a href="/store/outdoor-recreation/hiking">Hiking</a>  
                <ul>  
                <li><a href="/store/outdoor-recreation/hiking/snowshoeing">Snowshoeing</a></li>
                </ul>  
            </li>  
            <li><a href="/store/outdoor-recreation/skiing">Skiing</a></li>  
            </ul>  
        </li>
        <li><a href="/store/physical-fitness">Physical Fitness</a></li>  
        <li><a href="/store/provident-living">Provident Living</a></li>  
        </ul>  
    </li>  
</ul>  

All of the examples I've seen begin with a data structure that reflects the parent-child relationship (e.g. xml or JSON), but I'm having a very difficult time pulling this out of the URL and using it to render the new structure.

If anyone could please steer me in the right direction for how to do this using jQuery, I'd really appreciate it. I realize I probably need to use some recursive functions or maybe jQuery templates, but these things are still a bit new to me.
Thanks

Aurochs answered 20/8, 2011 at 18:9 Comment(0)
N
8

I think the best solution is firstly to convert your data structure to a tree one, with parent/children relations. Render this structure will then be easier, as the UL itself has a tree structure.

You can convert menuItems using these couple of functions

// Add an item node in the tree, at the right position
function addToTree( node, treeNodes ) {

    // Check if the item node should inserted in a subnode
    for ( var i=0; i<treeNodes.length; i++ ) {
        var treeNode = treeNodes[i];

        // "/store/travel".indexOf( '/store/' )
        if ( node.url.indexOf( treeNode.url + '/' ) == 0 ) {
            addToTree( node, treeNode.children );

            // Item node was added, we can quit
            return;
        }
    }

    // Item node was not added to a subnode, so it's a sibling of these treeNodes
    treeNodes.push({
        name: node.name,
        url: node.url,
        children: []
    });
}

//Create the item tree starting from menuItems
function createTree( nodes ) {
    var tree = [];

    for ( var i=0; i<nodes.length; i++ ) {
        var node = nodes[i];
        addToTree( node, tree );
    }

    return tree;
}

var menuItemsTree = createTree( menuItems );
console.log( menuItemsTree );

The resulting menuItemsTree will be an object like this

[
  {
    "name":"Store",
    "url":"/store",
    "children":[
      {
        "name":"Travel",
        "url":"/store/travel",
        "children":[

        ]
      },
      {
        "name":"Gardening",
        "url":"/store/gardening",
        "children":[

        ]
      },
      {
        "name":"Healthy Eating",
        "url":"/store/healthy-eating",
        "children":[
          {
            "name":"Cook Books",
            "url":"/store/healthy-eating/cook-books",
            "children":[

            ]
          },
          {
            "name":"Single Meal Gifts",
            "url":"/store/healthy-eating/single-meal-gifts",
            "children":[

            ]
          }
        ]
      },
      {
        "name":"Outdoor Recreation",
        "url":"/store/outdoor-recreation",
        "children":[
          {
            "name":"Hiking",
            "url":"/store/outdoor-recreation/hiking",
            "children":[
              {
                "name":"Snowshoeing",
                "url":"/store/outdoor-recreation/hiking/snowshoeing",
                "children":[

                ]
              }
            ]
          },
          {
            "name":"Skiing",
            "url":"/store/outdoor-recreation/skiing",
            "children":[

            ]
          }
        ]
      },
      {
        "name":"Physical Fitness",
        "url":"/store/physical-fitness",
        "children":[

        ]
      },
      {
        "name":"Provident Living",
        "url":"/store/provident-living",
        "children":[

        ]
      }
    ]
  }
]

You mentioned you already have html renderer for trees, right? If you need further help let us know!

Neapolitan answered 1/11, 2011 at 12:2 Comment(4)
This works very well, but one 'exception': If there's a subnode without a defined parent node it will be added like a parent/mainnode. I don't know if this is a wanted side-effect, but it would be plausible.Postdate
Sorry but this doesn't work, I tried your code several times. It works fine for the the first node but when there are children in the immediate next sibling- the next sibling's children gets added to the first node's children's list.Dopester
@Davide, How to using the shaped HTML data to render for a tree?Manamanacle
again, recursive function!Manamanacle
S
3

12 simple lines of code:

var rootList = $("<ul>").appendTo("body");
var elements = {};
$.each(menuItems, function() {
    var parent = elements[this.url.substr(0, this.url.lastIndexOf("/"))];
    var list = parent ? parent.next("ul") : rootList;
    if (!list.length) {
        list = $("<ul>").insertAfter(parent);
    }
    var item = $("<li>").appendTo(list);
    $("<a>").attr("href", this.url).text(this.name).appendTo(item);
    elements[this.url] = item;
});

http://jsfiddle.net/gilly3/CJKgp/

Submit answered 4/11, 2011 at 23:36 Comment(0)
M
2

Although I like the script of gilly3 the script produces list with different element nesting of <li> and <ul> than was originally asked. So instead of


   <li><a href="/store">Store</a>
     <ul>
        <li><a href="/store/travel">Travel</a></li>
        ...
     </ul>
   </li>
It produces

   <li><a href="/store">Store</a>
   </li>
   <ul>
      <li><a href="/store/travel">Travel</a></li>
      ...
   </ul>
This may cause incompatibilities for utilities or frameworks working with such generated menu and producing interactive menu with animation (e.g. superfish.js). So I updated the 12 lines script
var rootList = $("<ul>").appendTo("body");
var elements = {};
$.each(menuItems, function() {
    var parent = elements[this.url.substr(0, this.url.lastIndexOf("/"))];
    var list = parent ? parent.children("ul") : rootList;
    if (!list.length) {
        list = $("<ul>").appendTo(parent);
    }
    var item = $("<li>").appendTo(list);
    $("<a>").attr("href", this.url).text(this.name).appendTo(item);
    elements[this.url] = item;
});

http://jsfiddle.net/tomaton/NaU4E/

Menology answered 4/1, 2013 at 14:3 Comment(0)
H
0

It's not in jQuery, but maybe this could help. I developed this after seeking the web to do exactly what you want.

http://www.chapleau.info/article/ArrayofUrlsToASitemap.html

Hemialgia answered 31/10, 2011 at 17:17 Comment(0)
D
0

try something like this.

function Directory(parentNode) {
    //Structure for directories.  Subdirectories container as a generic object, initially empty
    this.hasSubdirectories = false;
    this.subdirectories = {};

    //Render in steps.  Until subdirectories or a link are added, all it needs is an LI and a blank anchor
    this.nodeLi = document.createElement("li");
    parentNode.appendChild(this.nodeLi);
    this.nodeA = document.createElement("a");
    this.nodeLi.appendChild(this.nodeA);

    //if a subdirectory is added, this.nodeUl will be added at the same time.
}

Directory.prototype.setLabel = function (sLabel) {
    this.nodeA.innerHTML = sLabel;
}

Directory.prototype.setLink = function (sLink) {
    this.nodeA.href = sLink;
}

Directory.prototype.getSubdirectory = function (sPath) {
    //if there were no previous subdirectories, the directory needs a new UL node.
    if (!this.hasSubdirectories) {
        this.nodeUl = document.createElement("ul");
        this.nodeLi.appendChild(this.nodeUl);
        this.hasSubdirectories = true;
    }

    //split the path string into the base directory and the rest of the path.
    var r = /^\/?(?:((?:\w|\s|\d)+)\/)(.*)$/;
    var path = r.exec(sPath);

    //if the desired path is in a subdirectory, find or create it in the subdirectories container.

    var subDirName = path[1] || path[2];
    var subDir;
    if (this.subdirectories[subDirName] === undefined) this.subdirectories[subDirName] = new Directory(this.nodeUl);
    subDir = this.subdirectories[subDirName];

    if (path[1] && path[2]) {
        return subDir.getSubdirectory(path[2]);
    } else {
        return subDir;
    }
}

function main(whichNode, aMenuItems) {
    //whichNode is the node that is to be the parent of the directory listing.
    //aMenuItems is the array of menu items.
    var i;
    var l = aItems.length;
    var topDir = new Directory(whichNode);

    //for each menu item, add a directory and set its properties.
    var dirToAdd;
    for (i = 0; i < l; i++) {
        dirToAdd = topDir.getSubdirectory(aMenuItems[i].url);
        dirToAdd.setLabel(aMenuItems[i].name);
        dirToAdd.setLink(aMenuItems[i].url);
    }

    //and that's it.
}

how's that work?

Dwelling answered 2/11, 2011 at 5:4 Comment(0)
T
0

Or maybe complete jQuery plugin http://jsfiddle.net/9FGRC/

(EDIT)

An update to previous version http://jsfiddle.net/9FGRC/1/

This version supports following case

var menuItems = [  
    {  
        name : "Store",  
        url : "/store"  
    },  
    {  
        name : "Cook Books",  
        url : "/store/healthy-eating/cook-books"  
    },  
    {  
        name : "Single Meal Gifts",  
        url : "/store/healthy-eating/single-meal-gifts"  
    }  
]  

Since there is skipped

    {  
        name : "Healthy Eating",  
        url : "/store/healthy-eating"  
    },

It will produce following html

<ul>
    <li><a href="/store">Store</a>
        <ul>
            <li><a href="/store/healthy-eating/cook-books">Cook Books</a></li>
            <li><a href="/store/healthy-eating/single-meal-gifts">Single Meal Gifts</a></li>
        </ul>
    </li>
</ul>

I guess it won't be the case, but could be helpful to someone

Theseus answered 2/11, 2011 at 8:14 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.