@suggestions   @rss   @archive   @codeforpeople.com     @random   @radio[:m3u|:pls|:ruby]   @family   @neighbors  @twitter 



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:

class FooController < ApplicationController
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
class BarController < ApplicationController
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 ActionController
  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!

Comments (View)
blog comments powered by Disqus