AngularJS multilevel dropdown menu for a menu structure generated from a recursive directive
Asked Answered
K

1

8

I have a dilly of a pickle here. I have to get my multi-level navigation menu from a webservice call.

Since my navigation menu can have an infinite amount of submenu's in it, I had to use a recursive directive to build my parent/child navigation structure. Now I am trying to figure out how to turn it into a functional dropmenu structure. I was taking a look at angularui-bootstrap, and they have a Dropdown Toggle, which has some basic dropmenu functionality, but since I used a recursive directive my menu structure already has angularjs generated css classes attached to them. The angularjs-bootstrap dropmenu's have css classes which are different than my angularjs generated classes....behold!

<ul>
    <li ng-repeat="parent in parents" class="ng-scope">
        <recursive-list-item on-node-click="onNodeClickFn(node)" parent="parent" class="ng-isolate-scope ng-scope">
            <a data-ng-click="onNodeClick({node: parent})" href="javascript:void(0)" class="ng-scope ng-binding">Clothes</a>
            <!-- ngIf: parent.children.length > 0 -->
            <ul data-ng-if="parent.children.length &gt; 0" class="ng-scope">
                <!-- ngRepeat: child in parent.children -->
                <li ng-repeat="child in parent.children" class="ng-scope">
                    <recursive-list-item data-on-node-click="onNodeClickFn(node)" data-parent="child" class="ng-isolate-scope ng-scope">
                        <a data-ng-click="onNodeClick({node: parent})" href="javascript:void(0)" class="ng-scope ng-binding">Gortex Jackets</a>
                        <!-- ngIf: parent.children.length > 0 -->
                    </recursive-list-item>
                </li>
                <!-- end ngRepeat: child in parent.children -->
                ...
                ...
                ...
            </ul>
        </recursive-list-item>
    </li>
    <!-- end ngRepeat: child in parent.children -->
...
...
</ul>

That was an example of the html that is generated as the final output for my recursive navigation menu. Having it set up this way all submenu's ng-click's are active, and they have the same scope as the main controller (all is dandy except it doesn't look like a dropmenu)

Here is an example of the dropmenu structure for angularjs-bootstrap

<li class="dropdown" ng-controller="DropdownCtrl">
  <a class="dropdown-toggle">
    Click me for a dropdown, yo!
  </a>
  <ul class="dropdown-menu">
    <li ng-repeat="choice in items">
      <a>{{choice}}</a>
    </li>
  </ul>
</li>

It has very much different css class structure than mine, so angularjs-bootstrap's 'dropdown' won't work with mine.

Does anyone have any suggestions for me? Keep in mind that since I am getting my navigation structure via json via a webservice call I have to use recursive angularjs in order to create the parent/child menu structure.

If anyone is confused about my directive html generated here I can show my custom directive code, but will not unless asked for brevity. My custom directive code works for just building the navigation structure, and keeps all the directive scopes connected to the main controller's scope (ie: click active), but it is not styled/scroll active.

My Non Functional Menu Navigation

*****UPDATE******** I have created a plunker replication that is almost the same. In my project I was getting my navigation menu data from an angularjs service, which would make a webservice call to a rest webservice on my server, but I don't have that here, so I just manually created the json for each one of my services that makes REST webservice calls. The important part is the recursive directive. Right below you will find the link to the plunker project. Can someone help me out?

Plunker Project -------------------------------------------------------------

*************NEWER UPDATE***************** Comment from Charlietfl that I can just have multiple css classes in my navigation dropmenu structure. I am attempting this with angularui-bootstrap. I followed the instructions for adding this to my project and created a new Plunker project based on the old plunker project, but with the extra dropmenu css classes added to the navigation structure. Here is the Plunker Project: Plunker Project

The navigational elements still show up in the DOM but they are not visible. I looked at the css for the first ul element and it is as such:

*, *:before, *:after {
    -moz-box-sizing: border-box;
}
*, *:before, *:after {
    -moz-box-sizing: border-box;
}
.dropdown-menu {
    background-clip: padding-box;
    background-color: #FFFFFF;
    border: 1px solid rgba(0, 0, 0, 0.15);
    border-radius: 4px;
    box-shadow: 0 6px 12px rgba(0, 0, 0, 0.176);
    display: none;
    float: left;
    font-size: 14px;
    left: 0;
    list-style: none outside none;
    margin: 2px 0 0;
    min-width: 160px;
    padding: 5px 0;
    position: absolute;
    top: 100%;
    z-index: 1000;
}

It was obtained from the official bootstrap css file. Not sure exactly why it's not visible. Not sure if it will help, but here is the css for the very next li element after the ul

*, *:before, *:after {
    -moz-box-sizing: border-box;
}
*, *:before, *:after {
    -moz-box-sizing: border-box;
}
.dropdown {
    position: relative;
}
.dropup, .dropdown {
    position: relative;
}
li {
    line-height: 20px;
}
*, *:before, *:after {
    -moz-box-sizing: border-box;
}

Keep in mind that you have to go to the plunker page to see the updated code starting from when I added the css tags needed for angularui-bootstrap. To see the invisible navigational elements you will need something like Firebug to see the DOM.

Here is an example of some html final output (from the DOM) from my update to try and work with angularui-bootstrap css classes.

...
<li ng-repeat="child in parent.children" class="dropdown ng-scope">
            <recursive-list-item data-on-node-click="onNodeClickFn(node)" data-parent="child" class="ng-isolate-scope ng-scope">
            <a class="dropdown-toggle ng-scope ng-binding" href="javascript:void(0)">Kids Clothes</a>
...

I suspect that the reason the angularui-bootstrap libary is not working is because of the "recursive-list-item.." element that is a child element of the "li" element, and parent element to the "a" element. Is this hunch of mine correct?

Kindliness answered 24/11, 2013 at 19:35 Comment(4)
not understanding why classes matter. An element can have numerous classes. Also you can modify or not even use the CSS from ui dropwdownCheerless
I had similar problem and solved it using this: #11855014 It helps you make angular-bootstrap structure. I had a problem with items duplication when refreshing from service but its for a new question.Hummock
Charlietfl, hello, I was not thinking along the lines of multiple css classes per element. I added the proper classes as per documentation angular-ui.github.io/bootstrap/#/getting_started and now the whole thing is invisible. The element structure is there in the DOM (firebug), but you can't see any of the categories in the navigation structure. I added 'ui.bootstrap' as a dependency to my controller's module and added ui-bootstrap-tpls-0.7.0.min.js to my index.html page, I have also added bootstrap.css, and even added that extra css line as per documentation. Any ideas?Kindliness
Charlietfl, I created a separate Plunker project that just has the changes to add the css classes to the necessary html. plnkr.co/edit/f0H3Yg?p=previewKindliness
B
7

This is what I use and it has a lot of extra functionality that is pretty sweet. See the usage $scope.menu and what happens when you expand the dropdown - you can put in headers, dividers, and even attach click functions. Note that you can nest as many ul as you want, and though toggle does work, it's useless since clicking to open a submenu will hide its parent. As far as I know, you would need to create your own javascript handler or custom css using hovers if you want deeper nesting in the menu.

Live demo here (click).

<nav>
  <div menu="menu"></div> <!-- the element here doesn't matter -->
</nav>

js:

var app = angular.module('myApp', ['ui.bootstrap']);

app.directive('menu', function() {
  return {
    restrict: 'A',
    scope: {
      menu: '=menu',
      cls: '=ngClass'
    },
    replace: true,
    template: '<ul><li ng-repeat="item in menu" menu-item="item"></li></ul>',
    link: function(scope, element, attrs) {
      element.addClass(attrs.class);
      element.addClass(scope.cls);
    }
  };
});

app.directive('menuItem', function($compile) {
  return {
    restrict: 'A',
    replace: true,
    scope: {
      item: '=menuItem'
    },
    template: '<li active-link><a href={{item.href}}>{{item.title}}</a></li>',
    link: function (scope, element, attrs) {
      if (scope.item.header) {
        element.addClass('nav-header');
        element.text(scope.item.header);
      }
      if (scope.item.divider) {
        element.addClass('divider');
        element.empty();
      }
      if (scope.item.submenu) {
        element.addClass('dropdown');

        var text = element.children('a').text();
        element.empty();
        var $a = $('<a class="dropdown-toggle">'+text+'</a>');
        element.append($a);

        var $submenu = $('<div menu="item.submenu" class="dropdown-menu"></div>');
        element.append($submenu);
      }
      if (scope.item.click) {
        element.find('a').attr('ng-click', 'item.click()');
      }
      $compile(element.contents())(scope);
    }
  };
});

app.controller('myCtrl', function($scope) {
  $scope.menu = [
    {
      "title": "Home",
      "href": "#"
    },
    {
      "title": "About",
      "href": "about"
    },
    {
      "title": "History",
      "href": "about/history"
    },
    {
      "title": "Contact",
      "href": "contact"
    },
    {
      "title": "Other things - in a list. (Click here)",
      "submenu": [
        {
          "header": "Sample Header"
        },
        {
          "title": "Some Link",
          "href": "some/place"
        },
        {
          "title": "Another Link",
          "href": "some/other/place"
        },
        {
          "divider": "true"
        },
        {
          "header": "Header 2"
        },
        {
          "title": "Again...a link.",
          "href": "errrr"
        },
        {
          "title": "Nest Parent",
          "submenu": [
            {
              "title": "nested again",
              "href": "nested/again"
            },
            {
              "title": "me too",
              "href": "sample/place"
            }
          ]
        }
      ]
    }
  ];
});

Update for nested dropdown:

Live demo here (click).

.dropdown-menu .dropdown-menu {
  margin: 0;
  left: 100%;
  top: -5px;
}

.dropdown-menu li:hover .dropdown-menu {
  display: block;
}
Broadside answered 24/11, 2013 at 21:18 Comment(3)
Ok, I checked out your demo. The parent menu does close when you click to open a child menu. Seems like the library should handle this for submenus....Kindliness
@thebravedave yeah, unfortunately, I'm almost totally positive that it isn't meant to. One thing to consider is that nested submenus like that are kind of bad practice. After one level, it's kind of a stress to the user (how many more of these do I have to click through!?). You could easily create your own functionality here, though.Broadside
well optimally you would have some sort of hover functionality too.Kindliness

© 2022 - 2024 — McMap. All rights reserved.