Creating dynamic popup menus with JQuery Mobile

Creating popup menus in JQuery Mobile is not quite simple. I am talking about menu that you might want to display when user clicks on, for example, a list item or a button. Refer to JQuery Mobile documents for select menus and custom menus . These menus are actually drop down lists and in all the cases, the menu has to be already visible.

However this is not what I wanted in my mobile apps. I do not want menu to be visible initially, but want to display it when user clicks on a button on a list item. So I started investigating how this could be done. As it turned out, the menu that JQuery Mobile shows (when nativeMenu attribute is set to false), is different from the select tags that you create for the menu. JQM hides the menu that you create and creates a new (wrapper) menu using div and anchor tags. Option tags become anchor tags in the new menu. This is all fine, but the problem is that JQM does not have any direct APIs to access elements of the new menu. This makes it difficult to attach event handlers to menu options.

But JQM follows certain patterns when creating the wrapper menu. As of the current version, which is 1.1, JQM does following –

  1. It creates an outer div with ui-selectmenu class
  2. Within the above div it creates ul element with id = id_of_the_select_element + “-menu”
  3. It creates li element for each menu option inside the above ul element
  4. Actual menu items are in anchor element inside the above li element

All this is not documented by JQuery Mobile, so it might change in the future versions.

So, having understood the above pattern, it was then easy to create a menu and attach event handler. Here is a simple example, which displays a menu when a button is clicked –

First a screen shot –

<!DOCTYPE html>
<html>
    <head>
        <link type="text/css" href="jquery.mobile-1.2.0.css" rel="stylesheet">

        <script type="text/javascript" src="jquery-1.8.2.js"></script>
        <script type="text/javascript" src="jquery.mobile-1.2.0.js"></script>

        <script type="text/javascript">
            $(document).bind("pageinit", onPageInit);

            //variable to store menu instance
            newMenu = null;

            function onPageInit()
            {
                //create menu. First option in the third argument is menu title
                createMenu("dynamicMenu1","contentDiv","Menus,menu1,menu2,menu3",menuHandler);

                $("#btn1").click(function(){
                    showMenu(newMenu);
                });
            }

            function createMenu(menuId, parentId, options, menuHandler)
            {
                //create a containing div
                var div = $("<div id='" + menuId + "div'></div>").appendTo("#"+parentId).hide();

                //create select tag
                var menuElm = $("<select id='" + menuId + "' data-inline='true' data-native-menu='false'></select>").appendTo(div);

                //add options
                var optionsArray = options.split(",");
                for (var i = 0; i < optionsArray.length; i++)
                    $("<option>" + optionsArray[i] + "</option>").appendTo("#"+menuId);

                //convert to JQueryMobile menu
                $("#" + menuId).selectmenu();

                //find custom menu that JQM creates
                var menus = $(".ui-selectmenu");
                for (var i = 0; i < menus.length; i++)
                {
                    //if ($(menus[i]).children("ul:#" + menuId + "-menu").length > 0)
                    if ($(menus[i]).children("ul").filter("#" + menuId + "-menu").length > 0)
                    {
                        newMenu = $(menus[i]);
                        break;
                    }
                }

                //Hack for JQM 1.2 - check if parent of select menu is ui-popup-container
                var menuContainer = $(menus).parent(".ui-popup-container");
                if (menuContainer.length > 0)
                {
                    var pageElm = menuContainer.parent("div[data-role='page']");
                    if (pageElm.length > 0)
                    {
                        menus.detach();
                        menuContainer.remove();
                        pageElm.append(menus);
                        menus.css("width", "80%");
                        menus.css("max-width", "350px");
                    }
                }

                if (newMenu == null)
                {
                    alert("Error creating menu");
                    return;
                }

                //Associate click handler with menu items, i.e. anchor tags
                $(newMenu).find(".ui-selectmenu-list li a").click(menuHandler);

                //Add Close option
                var menuHeader = $(newMenu).find(".ui-header");
                var closeLinkId = menuId + "_close_id";
                menuHeader.prepend("<span style='position:relative;float:left'>" +
                    "<a href='#' id='" + closeLinkId + "'>X</href></span>");
                $("#" + closeLinkId).click(function(e){
                    newMenu.hide();
                });

                return newMenu.hide();
            }

            function showMenu(menu)
            {
                if (menu == null)
                    return;

                //show menu at center of the window
                var left = ($(window).width() - $(menu).width()) / 2;
                //consider vertical scrolling when calculating top
                var top = (($(window).height() - $(menu).height()) / 2) + $(window).scrollTop();
                $(menu).css({
                    left: left,
                    top: top
                });

                $(menu).show();
            }

            //Callback handler when menu item is clicked
            function menuHandler(event)
            {
                if (newMenu != null)
                    $(newMenu).hide();

                alert(event.srcElement.text);
            }
        </script>

    </head>

    <body>
        <div data-role="page" >
            <div data-role="content" id="contentDiv">
                <a href="" data-role="button" data-inline="true" id="btn1">Show Menu</a>
            </div>
            <span style='position:relative;float:left'></span>
        </div>

    </body>
</html>

– Ram Kulkarni

30 Replies to “Creating dynamic popup menus with JQuery Mobile”

  1. Ram – very nice, this saved me a lot of headache! Curious, what if I wanted to capture a value of a list item, or even the index of which item in the list. I’m not finding an easy way to do that.

    1. Here is how you can attach any data with menu item and retrieve it when it is clicked. In the createMenu function, add this towards the end –
      ——————
      //Get all menu anchors
      var menuAnchors = $(newMenu).find(“.ui-selectmenu-list li a”);
      for (var i = 0; i < menuAnchors.length; i++) { //I am associating index with each menu item $(menuAnchors[i]).data("index",i); } ------------------ Then in the menu handler you can retrieve it as - alert(event.srcElement.text + " " + $(event.srcElement).data("index"));

  2. And one other question. My app needs to call the select menu possibly a few times, with different items in the list each time possibly. I attempted to do a $(newMenu).remove() after it is closed, but the next time createmenu is called, it does not like that. Is there a clean way to destroy and recreate the menu each time?

  3. I am trying to extend it so that I can add anchor tags to the list items, but I can’t find a convenient way to do this as it is. I could pass the create function an array or a string as you did and then add them to the options, but somehow I don’t like the idea.
    Have you got an idea how to do this “the smart way”? 😉

    Great job by the way. The jQuery Mobile select menu is quite useless if you can’t access it programmatically and you addressed that. Thanks for that.

    1. Daniel,
      JQM finally creates anchor tags for each list items. For example the list item for ‘menu1’ option in the example I posted in the blog is created by JQM as –

      <li data-option-index="1" data-icon="false" class="ui-btn ui-btn-icon-right ui-li ui-btn-up-c" role="option" data-corners="false" data-shadow="false" data-iconshadow="true" data-wrapperels="div" data-iconpos="right" data-theme="c" aria-selected="false">
          <div class="ui-btn-inner ui-li">
              <div class="ui-btn-text">
                  <a href="#" tabindex="-1" class="ui-link-inherit">menu1</a>
              </div>
          </div>
      </li>
      

      It might help if you describe how you want to add anchor tags.

    1. I have used a global variable newMenu, but if you make it a local variable inside createMenu function, then you should be able to create multiple menus by calling createMenu function. This function already returns the menu instance.

    1. I have updated the code to add ‘X’ to the menu header to close it. I am no expert in CSS, so could not style it properly.
      This is the snippet of code I added towards the end of createMenu function –

      //Add Close option
      var menuHeader = $(newMenu).find(".ui-header");
      var closeLinkId = menuId + "_close_id";
      menuHeader.prepend("<span style='position:relative;float:left'>" +
          "<a href='#' id='" + closeLinkId + "'>X</href></span>");
      $("#" + closeLinkId).click(function(e){
          newMenu.hide();
      });
      
          1. Here is how you can make options scrollable –

            if (scrollable)
            {
                //Make menu options scrollable
                var menuList = $(newMenu).find("ul.ui-selectmenu-list");
                var scrollableDivStr = "<div style='height:" + scrollableDivHeight + ";overflow:auto'></div>";
                var scrollableDiv = $(scrollableDivStr).insertBefore(menuList);
                menuList.detach();
                scrollableDiv.append(menuList);
            }
            

            I have also updated the post.

    1. I tested it in Chrome and IE and works fine. You may have to debug the problem at your end. Note that if height of the scrollable div (as specified in scrollableDivHeight variable) is greater than height of the menu, then scrollbar won’t appear. And I am assuming that you have set variable scrollable to true.

    1. Yes, it does not work on mobile. I have removed this code from the post. Actually if there are many menu items, then the menu can be scrolled with the entire page. But I guess you don’t want that.

      The code that I posted earlier for scrolling kind of works on mobile – menu items are displayed in a div with fixed height, but scroll bar is not displayed. So there is no way to scroll menu items. I can’t think of any quick and easy solution.

  4. Hi, I was testing using jquery-1.8.1 and it is not working, it failed when trying to find custom menu created by JQM, in this line

    if ($(menus[i]).children(“ul:#” + menuId + “-menu”).length > 0)

    log said:

    jquery-1.8.1.js:4642Error: Syntax error, unrecognized expression: ul:#dynamicMenu1-menu

    Would you please help me?

    1. If I understand your question correctly, you are asking how to show menu on JQuery chart. If so, the process is similar to what I described in the post. It is two step process – first you create the menu, with createMenu function. Once created, it can be displayed by calling showMenu function. In your case, you may call it in any event handler of the chart.

  5. HI Ram, firstly great work on the popup menu is just was I was looking for. Just to follow on from @Wills comment re: JQM 1,2.0 even after the change to the filter it still doesn’t show the menu.

    After inspecting the html I can see a parent div with class ui-selectmenu-hidden that kind of shifts the menu way off screen. If you select the parent and remove this class you can get it to appear by adjusting the top and left (not sure if this is the best approach).

    I think it would be worthwhile updating your post with the latest versions of JQuery and JQM as it is still a great solution for a simple to build dynamic popup menu.

    Cheers

    Jono

  6. Hi, Ram, this is the best solution I found after several days of reading. Would it be possible to filter menu data? Tnx in advance!

    1. You can write code to filter in showMenu method, just before $(menu).show(). For example, if I want to hide menu2, then I would do something like –

      var filter = "menu2";
      
      $(menu).find("a:contains('" + filter + "')").parent().hide();
      

      Or you can get all anchor tags of pop-up menu and filter them individually –

      var menuItems = $(menu).find("a");
      for (var i = 0; i < menuItems.length; i++)
      {
          if (menuItems[i].text == filter)
              $(menuItems[i]).parent().hide();
      }
      
    1. If you are asking where you could use pop-up/dropdown menu on your website, then I would say I am not an expert in UEx design. If you are asking if pop-up menu explained in this post could be used on the web site, then the answer is yes. But note that the menu is created with JQuery Mobile and meant for mobile sites. Having said that, nothing stops you from using it on desktop sites.

Leave a Reply