Reinventing Rails’ components, Part Deux.
I got quite a response from my initial posting on reinventing Rails’ components. After a lot of coffee and converstaion with my good friends Dave Clements, Jeremy Hinegardner, and Dan Fitzpatrick I’ve managed to refine it a little bit. First an example and the latest componentry.rb:
public
def foobar
bc = component_for BarController
foo_content = bc.content_for ‘foo’, :foo => 42
bar_content = bc.content_for ‘bar’, :bar => ‘forty-two’
render :text => [ foo_content, bar_content ].inspect
# :text => [ 42, ”forty-two” ]
end
end
public
def foo
setup
render :text => params[:foo]
end
def bar
setup
render :text => params[:bar]
end
private
def setup
@records ||= “SOME_GIANT_ACTIVE_RECORD_QUERY!”
end
end
module Components
module InstanceMethods
#
# returns a child component with both initializaion and defered-rendering ability
#
def component_for(controller_name_or_constant, params = {}, &block)
controller = controller_name_or_constant
params.to_options!
parent_controller = params.delete(:parent_controller) || self
options = { :controller => controller, :params => params }
klass = component_class options
returning(klass.new) do |controller|
singleton_class =
class « controller
self
end
singleton_class.class_eval do
define_method(:parent_controller){ parent_controller }
define_method(:content_for) do |action_name, *argv|
params = ( argv.shift || {} ).to_options!
options = { :action => action_name.to_s, :params => params }
parent_controller.send(:component_logging, options) do
request = parent_controller.instance_eval do
request_for_component(klass.controller_name, options)
end
reuse_response = options.delete(:reuse_response)
response = reuse_response ? parent_controller.response : parent_controller.response.dup
response = process(request, response)
if redirected = response.redirected_to
content_for(redirected)
else
response.body
end
end
end
end
controller.parent_controller = parent_controller
controller.instance_eval{ setup if defined? setup }
controller.instance_eval(&block) if block
end
end
end
end
end
Now, besides the syntax being cleaned up there are few things to notice here. First off is that the BarController is usable from outside the FooController - we could easily hit it with ajax reqeusts to pull back html viewlets. Secondly, notice how the FooController uses its component - it first contructs the object and then uses two actions, ‘foo’ and ‘bar’, to generate content. Seems like we could do this with ‘render_component_as_string’ right? Not quite. Read the source for ‘component_for’ above and you’ll see that the ‘setup’ methods is invoked if, and only if, it is defined in the controller class. This means that, when used as a component, the database will only be hit once for each of the two actions rendered.
Now I’ve just got to get componentry.rb out the door and up to rubyforge!