Separating Rails Layout Associations

Written on 9:16:00 PM by S. Potter

I just realized I hadn't shared this code yet on this blog. I will also be including it in one of the metafusion subprojects (coming soon). I've been using it when needed in my Rails projects on and off for the last several months.

The Problem

You have some controllers you include into your application from plugins or engines and you want to associate a particular layout to the plugin/engine controller from within your application (without changing anything in the plugin or engine - of course!). There is not good way of doing this nicely in Rails presently as each controller usually defines controller-wide layouts within its definition.

The Solution


# See:
# * Finsignia::Paths

# Contains helper methods related to paths and resolving modules and 
# classes from paths.  Provides for helpful mixin for various applications.
module Finsignia::Paths

  def self.included(base)
    base.extend ClassMethods
  # Contains class methods for Finsignia::Paths mixin
  module ClassMethods
    @@element_postfixes = {:model => '', :controller => 'Controller'}

    def resolve_model(path)
      resolve_element :model, path
    def resolve_controller(path)
      resolve_element :controller, path
    def normalize_module_name(path)
      list = path.split('_')
      list.collect do |item|
    # Resolves path to a type of Rails element 
    # (e.g. model, controller, etc.).
    # Path refers to 'internal' path, NOT require path:
    #  'users/users' #=> internal path
    #  'users/users_controller #=> require path
    def resolve_element(type, path)
      first, rest = path.split('/')
      mod = ObjectSpace.const_get(normalize_module_name(first))
      while rest
        first, rest = rest.split('/')

        unless rest
          mod = mod.const_get(normalize_module_name("#{first}_#{@@element_postfixes[type].downcase}"))
          mod = mod.const_get(normalize_module_name(first))
        # I doubt this will ever get this far as per expeceted behavior of ObjectSpace and commented it out following unreachable line.
        #raise"#{type} constant not found for internal path #{path}") unless mod
        return mod unless rest
    # Separating this so we can stub this method out in specifications.
    def require_controller(path)
      require("#{path}_controller") unless "test" == ENV["RAILS_ENV"]
    # Separating this so we can stub this method out in specifications.
    def require_model(path)
      require(path) unless "test" == ENV["RAILS_ENV"]


# See:
# * Finsignia::Layouts
# * Finsignia::LayoutsError

# Raised when an exceptional condition arises in the Layouts 
# mapping process.
class Finsignia::LayoutsError < Exception; end

# Provides a closure and declarative way to define layout
# mappings for an application that utilize controllers, views, 
# etc. from plugins.
# The closure approach simulates the Rails Routing approach 
# closely, like:
#  require 'application'
# do |map|
#    map.connect 'users/sessions', 'layout_name'
#  end
# The declarative approach looks like the following:
#  require 'application'
#  include Finsignia
# 'users/sessions', 'layout_name'
module Finsignia::Layouts
  class << self
    def connect(controller_path, layout)
      # resolve controller class and call .layout(layout) on it.
      controller = resolve_controller(controller_path)
    def map(&block)
      yield self if block_given?

#  private
    include Finsignia::Paths    

So basically in a file like config/layouts.rb of our application we have something like: do |map|
  map.connect 'users/sessions', 'session'
  map.connect 'users/users', 'users'
  map.connect 'accounts/transaction', 'account'

Then include the config/layouts.rb file in config/environment.rb and you have separated your concerns relatively nicely and easily. When I release metafusion-rails my plan is that instead of installing the finsignia/paths.rb and finsignia/layouts.rb in the lib directory you would be able to do something like the following at the end of your config/environment.rb file:

gem('metafusion-rails', '=MFR_VERSION')
require 'metafusion/rails' do |map|
  map.connect 'namespace/resource', 'layout'
  # etc....
Alternatively you could still keep the layouts.rb file if your environment.rb is getting large and keep the layouts.rb include in your environment.rb.

If you enjoyed this post Subscribe to our feed


  1. Dave Hoover |

    Very cool, Susan. Looking forward to seeing metafusion!

  2. Anonymous |

    Susan really incredible stuff. Loking forward for metafusion.


Post a Comment