Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

AngularJS multilevel dropdown menu for a menu structure generated from a recursive directive

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?

like image 690
David Pugh Avatar asked Nov 24 '13 19:11

David Pugh


1 Answers

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;
}
like image 174
m59 Avatar answered Nov 10 '22 02:11

m59