Search code examples
mysqlruby-on-railsdrop-down-menumemory-efficient

Multiple dropdown menus in rails 6


Quite new to rails and can't figure out how to build a dynamic menu bar. The database uses a simple method to store menu items:

id | title   | url             | parent | position
--------------------------------------------------------------------
1    Books     /books            0        1 (first link on menu bar)
2    Fiction   /books/fiction    1        1 (first link under Books menu)
3    CDs       /cd               0        2 (second link on menu bar)
4    Fantasy   /books/fantasy    1
5    Singles   /cd/single        2
6    Albums    /cd/album         2

I need to get all entries in this table and display them according to their parent. All the items with parent 0 will show on the menu bar. When user hovers any parent, all its child elements are listed below it.

When I did this in PHP, I used a loop that resulted in many database calls; One to get all the parent items in position order (as the admin can change the order in which the parent items appear on the menu), and then one for each parent to get all its child elements (also in position order). I then added each resulting item to an array that was named after the parent_id and finally concatonated the arrays.

$item[$parent][] = '<a href...'

I know the SQL was inefficient (and the PHP probably was, too) but it didn't matter back then. Now it does.

Any idea how I can make this work efficiently in ruby?


Solution

  • Let's assume that the database table that stores this menu information is called nav_items. Then the Rails convention would be to have a model called NavItem:

    # app/models/nav_item.rb
    class NavItem < ApplicationRecord
      has_many :child_menu_items, class_name: NavItem, inverse_of: :parent_menu_item, foreign_key: :parent
      belongs_to :parent_menu_item, class_name: NavItem, foreign_key: :parent, inverse_of: :child_menu_items
    
      scope :top_level, { where("parent = ?", 0) }
    end
    
    # app/controllers/application_controller.rb
    class ApplicationController < ActionController::Base
      before_action :populate_nav_vars
    
      private
      def populate_nav_vars
        @nav_items = NavItem.top_level
      end
    end
    
    # app/view/layouts/_nav.html
    # @nav_items is defined in the controller as NavItem.top_level
    # the nav structure is shown here as nested <ul> lists
    # it can be rendered appropriately with css (e.g. Bootstrap)
    <ul>
    <%- @nav_items.each do |top_level_item| %>
       <li><%= top_level_item.title %></li>
       <li>
         <ul>
           <%- top_level_item.child_menu_items.each do |child_item| %>
             <li><%= child_item.title %>
           <%- end %>
         </ul>
       </li>
    <%- end %>
    </ul>
    

    You really shouldn't worry about performance, it should not be noticeable at all. I assume here that you want the navigation menu on every page, so the convenient place to populate the required variables is in ApplicationController and populate the variable in a before_action callback.

    If the performance issue concerns you, then you should make the navigation page elements a view partial, and apply rails caching to the partial.