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]So basically in a file like config/layouts.rb of our application we have something like:# 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
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.

Very cool, Susan. Looking forward to seeing metafusion!
Susan really incredible stuff. Loking forward for metafusion.