Permalink
Browse files

Initial commit

  • Loading branch information...
lifo committed Aug 29, 2008
0 parents commit 4583103a64894093eb5f6cbafa3aedc543f8f61a
Showing with 353 additions and 0 deletions.
  1. +37 −0 README
  2. +22 −0 Rakefile
  3. +2 −0 init.rb
  4. +142 −0 lib/components.rb
  5. +10 −0 test/abstract_unit.rb
  6. +140 −0 test/components_test.rb
37 README
@@ -0,0 +1,37 @@
+Components allow you to call other actions for their rendered response while executing another action. You can either delegate
+the entire response rendering or you can mix a partial response in with your other content.
+
+ class WeblogController < ActionController::Base
+ # Performs a method and then lets hello_world output its render
+ def delegate_action
+ do_other_stuff_before_hello_world
+ render_component :controller => "greeter", :action => "hello_world", :params => { :person => "david" }
+ end
+ end
+
+ class GreeterController < ActionController::Base
+ def hello_world
+ render :text => "#{params[:person]} says, Hello World!"
+ end
+ end
+
+The same can be done in a view to do a partial rendering:
+
+ Let's see a greeting:
+ <%= render_component :controller => "greeter", :action => "hello_world" %>
+
+It is also possible to specify the controller as a class constant, bypassing the inflector
+code to compute the controller class at runtime:
+
+<%= render_component :controller => GreeterController, :action => "hello_world" %>
+
+== When to use components
+
+Components should be used with care. They're significantly slower than simply splitting reusable parts into partials and
+conceptually more complicated. Don't use components as a way of separating concerns inside a single application. Instead,
+reserve components to those rare cases where you truly have reusable view and controller elements that can be employed
+across many applications at once.
+
+So to repeat: Components are a special-purpose approach that can often be replaced with better use of partials and filters.
+
+Copyright (c) 2007 David Heinemeier Hansson, released under the MIT license
@@ -0,0 +1,22 @@
+require 'rake'
+require 'rake/testtask'
+require 'rake/rdoctask'
+
+desc 'Default: run unit tests.'
+task :default => :test
+
+desc 'Test the components plugin.'
+Rake::TestTask.new(:test) do |t|
+ t.libs << 'lib'
+ t.pattern = 'test/**/*_test.rb'
+ t.verbose = true
+end
+
+desc 'Generate documentation for the components plugin.'
+Rake::RDocTask.new(:rdoc) do |rdoc|
+ rdoc.rdoc_dir = 'rdoc'
+ rdoc.title = 'Components'
+ rdoc.options << '--line-numbers' << '--inline-source'
+ rdoc.rdoc_files.include('README')
+ rdoc.rdoc_files.include('lib/**/*.rb')
+end
@@ -0,0 +1,2 @@
+require 'components'
+ActionController::Base.send :include, Components
@@ -0,0 +1,142 @@
+module Components
+ def self.included(base) #:nodoc:
+ base.class_eval do
+ include InstanceMethods
+ extend ClassMethods
+ helper HelperMethods
+
+ # If this controller was instantiated to process a component request,
+ # +parent_controller+ points to the instantiator of this controller.
+ attr_accessor :parent_controller
+
+ alias_method_chain :process_cleanup, :render_component
+ alias_method_chain :set_session_options, :render_component
+ alias_method_chain :flash, :render_component
+ alias_method_chain :assign_shortcuts, :render_component
+ alias_method_chain :send_response, :render_component
+
+ alias_method :component_request?, :parent_controller
+ end
+ end
+
+ module ClassMethods
+ # Track parent controller to identify component requests
+ def process_with_components(request, response, parent_controller = nil) #:nodoc:
+ controller = new
+ controller.parent_controller = parent_controller
+ controller.process(request, response)
+ end
+ end
+
+ module HelperMethods
+ def render_component(options)
+ @controller.send!(:render_component_as_string, options)
+ end
+ end
+
+ module InstanceMethods
+ # Extracts the action_name from the request parameters and performs that action.
+ def process_with_components(request, response, method = :perform_action, *arguments) #:nodoc:
+ flash.discard if component_request?
+ process_without_components(request, response, method, *arguments)
+ end
+
+ def send_response_with_render_component
+ response.prepare! unless component_request?
+ response
+ end
+
+ protected
+ # Renders the component specified as the response for the current method
+ def render_component(options) #:doc:
+ component_logging(options) do
+ render_for_text(component_response(options, true).body, response.headers["Status"])
+ end
+ end
+
+ # Returns the component response as a string
+ def render_component_as_string(options) #:doc:
+ component_logging(options) do
+ response = component_response(options, false)
+
+ if redirected = response.redirected_to
+ render_component_as_string(redirected)
+ else
+ response.body
+ end
+ end
+ end
+
+ def flash_with_render_component(refresh = false) #:nodoc:
+ if !defined?(@_flash) || refresh
+ @_flash =
+ if defined?(@parent_controller)
+ @parent_controller.flash
+ else
+ flash_without_render_component
+ end
+ end
+ @_flash
+ end
+
+ private
+ def component_response(options, reuse_response)
+ klass = component_class(options)
+ request = request_for_component(klass.controller_name, options)
+ new_response = reuse_response ? response : response.dup
+
+ klass.process_with_components(request, new_response, self)
+ end
+
+ # determine the controller class for the component request
+ def component_class(options)
+ if controller = options[:controller]
+ controller.is_a?(Class) ? controller : "#{controller.camelize}Controller".constantize
+ else
+ self.class
+ end
+ end
+
+ # Create a new request object based on the current request.
+ # The new request inherits the session from the current request,
+ # bypassing any session options set for the component controller's class
+ def request_for_component(controller_name, options)
+ new_request = request.dup
+ new_request.session = request.session
+
+ new_request.instance_variable_set(
+ :@parameters,
+ (options[:params] || {}).with_indifferent_access.update(
+ "controller" => controller_name, "action" => options[:action], "id" => options[:id]
+ )
+ )
+
+ new_request
+ end
+
+ def component_logging(options)
+ if logger
+ logger.info "Start rendering component (#{options.inspect}): "
+ result = yield
+ logger.info "\n\nEnd of component rendering"
+ result
+ else
+ yield
+ end
+ end
+
+ def set_session_options_with_render_component(request)
+ set_session_options_without_render_component(request) unless component_request?
+ end
+
+ def process_cleanup_with_render_component
+ process_cleanup_without_render_component unless component_request?
+ end
+
+ def assign_shortcuts_with_render_component(request, response)
+ assign_shortcuts_without_flash(request, response)
+ flash(:refresh)
+ flash.sweep if @_session && !component_request?
+ end
+ end
+end
@@ -0,0 +1,10 @@
+# [TODO] Replace with requiring rails gem after we release 2.2
+$: << '/Users/lifo/commit-rails/rails/actionpack/lib'
+
+require 'test/unit'
+require 'action_controller'
+require 'action_controller/test_process'
+ActionController::Routing::Routes.reload rescue nil
+
+$: << File.dirname(__FILE__) + "/../lib"
+require File.dirname(__FILE__) + "/../init"
@@ -0,0 +1,140 @@
+require File.dirname(__FILE__) + '/abstract_unit'
+
+class CallerController < ActionController::Base
+ def calling_from_controller
+ render_component(:controller => "callee", :action => "being_called")
+ end
+
+ def calling_from_controller_with_params
+ render_component(:controller => "callee", :action => "being_called", :params => { "name" => "David" })
+ end
+
+ def calling_from_controller_with_different_status_code
+ render_component(:controller => "callee", :action => "blowing_up")
+ end
+
+ def calling_from_template
+ render :inline => "Ring, ring: <%= render_component(:controller => 'callee', :action => 'being_called') %>"
+ end
+
+ def internal_caller
+ render :inline => "Are you there? <%= render_component(:action => 'internal_callee') %>"
+ end
+
+ def internal_callee
+ render :text => "Yes, ma'am"
+ end
+
+ def set_flash
+ render_component(:controller => "callee", :action => "set_flash")
+ end
+
+ def use_flash
+ render_component(:controller => "callee", :action => "use_flash")
+ end
+
+ def calling_redirected
+ render_component(:controller => "callee", :action => "redirected")
+ end
+
+ def calling_redirected_as_string
+ render :inline => "<%= render_component(:controller => 'callee', :action => 'redirected') %>"
+ end
+
+ def rescue_action(e) raise end
+end
+
+class CalleeController < ActionController::Base
+ def being_called
+ render :text => "#{params[:name] || "Lady"} of the House, speaking"
+ end
+
+ def blowing_up
+ render :text => "It's game over, man, just game over, man!", :status => 500
+ end
+
+ def set_flash
+ flash[:notice] = 'My stoney baby'
+ render :text => 'flash is set'
+ end
+
+ def use_flash
+ render :text => flash[:notice] || 'no flash'
+ end
+
+ def redirected
+ redirect_to :controller => "callee", :action => "being_called"
+ end
+
+ def rescue_action(e) raise end
+end
+
+class ComponentsTest < Test::Unit::TestCase
+ def setup
+ @controller = CallerController.new
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+ end
+
+ def test_calling_from_controller
+ get :calling_from_controller
+ assert_equal "Lady of the House, speaking", @response.body
+ end
+
+ def test_calling_from_controller_with_params
+ get :calling_from_controller_with_params
+ assert_equal "David of the House, speaking", @response.body
+ end
+
+ def test_calling_from_controller_with_different_status_code
+ get :calling_from_controller_with_different_status_code
+ assert_equal 500, @response.response_code
+ end
+
+ def test_calling_from_template
+ get :calling_from_template
+ assert_equal "Ring, ring: Lady of the House, speaking", @response.body
+ end
+
+ def test_etag_is_set_for_parent_template_when_calling_from_template
+ get :calling_from_template
+ expected_etag = etag_for("Ring, ring: Lady of the House, speaking")
+ assert_equal expected_etag, @response.headers['ETag']
+ end
+
+ def test_internal_calling
+ get :internal_caller
+ assert_equal "Are you there? Yes, ma'am", @response.body
+ end
+
+ def test_flash
+ get :set_flash
+ assert_equal 'My stoney baby', flash[:notice]
+ get :use_flash
+ assert_equal 'My stoney baby', @response.body
+ get :use_flash
+ assert_equal 'no flash', @response.body
+ end
+
+ def test_component_redirect_redirects
+ get :calling_redirected
+
+ assert_redirected_to :controller=>"callee", :action => "being_called"
+ end
+
+ def test_component_multiple_redirect_redirects
+ test_component_redirect_redirects
+ test_internal_calling
+ end
+
+ def test_component_as_string_redirect_renders_redirected_action
+ get :calling_redirected_as_string
+
+ assert_equal "Lady of the House, speaking", @response.body
+ end
+
+ protected
+ def etag_for(text)
+ %("#{Digest::MD5.hexdigest(text)}")
+ end
+end

2 comments on commit 4583103

Contributor

dpowell replied Dec 21, 2008

You need to backport http://github.com/rails/rails/commit/a1eb4e11c2cccb91483fa15f1a1a0b2ae518d2cf#diff-4 on line 33 (send! has been removed from Rails after having been dropped from Ruby 1.0).

Member

lifo replied Dec 30, 2008

Done. Thanks

Please sign in to comment.