Search code examples
ruby-on-railspartials

Available approaches for layouts when only the navigation depends on the controller


Given a layout with a navigation bar which depends on the current controller :

# layout.html.slim
doctype html
html
  body
    main
      = render partial: 'domain_nav'
      = yield

The app has several business domains, let's say "Clients", "Tasks", "Books", with their own distinct navigations (clients_nav, tasks_nav, books_nav).

Each part has several controllers, and the view will display the navigation based on the controller business domain (e.g. the Clients::CompaniesController will display the clients_nav).

The html above is simplified but basically, all controllers share the same layout except for the domain_nav which depends on the domain.

I see several ways to handle that :

  • use a helper and determine which nav to use based on the controller name : this means editing the helper each time a new controller is introduced, it doesn't feel right
  • use content_for :navigation in each view and yield :navigation : since we're sure that the navigation is displayed for each view, this doesn't feel right either
  • use a sub layout per section and call render layout: 'clients/layout' do : it works but then i18n becomes a mess
  • define one layout per domain : not very DRY
  • use a single layout and define which nav (partial name) to use in the controller : that partial name would be set using either inheritance (have one controller per domain that sets the nav partial name) or concerns
  • define a layout per domain (setting the layout in each controller) but render them using a shared layout as shown below

# layouts/_shared.html.slim
doctype html
html
  body
    main
      = render partial: nav_path
      = yield

# layouts/clients.html.slim
= render partial: 'layouts/shared', locals: { nav_path: 'clients/nav' }

# layouts/tasks.html.slim
= render partial: 'layouts/shared', locals: { nav_path: 'tasks/nav' }

# layouts/books.html.slim
= render partial: 'layouts/shared', locals: { nav_path: 'books/nav' }

Are there other approaches to this problem ? Which one would you pick and why ?


Solution

  • I went a different way. Since it's the responsibility of the controller to decide of the layout, I thought it would be the rails way to let it also decide of the navigation.

    So just like there is a "layout" declaration, I now have a "navigation" declaration. Here's what the controllers look like:

    class ClientsController < ApplicationController
      navigation 'clients/nav'
      …
    end
    
    class BooksController < ApplicationController
      navigation 'books/nav'
      …
    end
    

    and I have just one layout:

    doctype html
    html
      body
        main
          = render partial: navigation
          = yield
    

    The navigation method is declared in ApplicationController:

    class ApplicationController < ActionController::Base
      helper_method :navigation
    
      def self.navigation(path)
        define_method :navigation do
          path
        end
      end
    end