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

[finsignia/paths.rb]

# 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
  end
  
  # Contains class methods for Finsignia::Paths mixin
  module ClassMethods
    @@element_postfixes = {:model => '', :controller => 'Controller'}

    def resolve_model(path)
      resolve_element :model, path
    end
  
    def resolve_controller(path)
      resolve_element :controller, path
    end
    
    def normalize_module_name(path)
      list = path.split('_')
      list.collect do |item|
        item.capitalize
      end.join
    end
    
    # 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}"))
        else
          mod = mod.const_get(normalize_module_name(first))
        end
        # I doubt this will ever get this far as per expeceted behavior of ObjectSpace and Module...so commented it out following unreachable line.
        #raise NameError.new("#{type} constant not found for internal path #{path}") unless mod
        return mod unless rest
      end      
    end
    
    # Separating this so we can stub this method out in specifications.
    def require_controller(path)
      require("#{path}_controller") unless "test" == ENV["RAILS_ENV"]
    end
    
    # Separating this so we can stub this method out in specifications.
    def require_model(path)
      require(path) unless "test" == ENV["RAILS_ENV"]
    end
  end
end

[finsignia/layouts.rb]

# 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'
#  
#  Finsignia::Layouts.map do |map|
#    map.connect 'users/sessions', 'layout_name'
#  end
#
# The declarative approach looks like the following:
#  require 'application'
#  include Finsignia
#  Layouts.map 'users/sessions', 'layout_name'
# 
module Finsignia::Layouts
  class << self
    def connect(controller_path, layout)
      require_controller(controller_path)
      # resolve controller class and call .layout(layout) on it.
      controller = resolve_controller(controller_path)
      controller.layout(layout)
    end
    
    def map(&block)
      yield self if block_given?
    end
  end

#  private
    include Finsignia::Paths    
end

So basically in a file like config/layouts.rb of our application we have something like:

Finsignia::Layouts.map do |map|
  map.connect 'users/sessions', 'session'
  map.connect 'users/users', 'users'
  map.connect 'accounts/transaction', 'account'
end

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'

Finsignia::Layouts.map do |map|
  map.connect 'namespace/resource', 'layout'
  # etc....
end
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

2 Comments

  1. Dave Hoover |

    Very cool, Susan. Looking forward to seeing metafusion!

     
  2. 2.0 myspace layouts mediation |

    Susan really incredible stuff. Loking forward for metafusion.

     

Post a Comment