Skip to content

Commit 4583103

Browse files
committed
Initial commit
0 parents  commit 4583103

File tree

6 files changed

+353
-0
lines changed

6 files changed

+353
-0
lines changed

README

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
Components allow you to call other actions for their rendered response while executing another action. You can either delegate
2+
the entire response rendering or you can mix a partial response in with your other content.
3+
4+
class WeblogController < ActionController::Base
5+
# Performs a method and then lets hello_world output its render
6+
def delegate_action
7+
do_other_stuff_before_hello_world
8+
render_component :controller => "greeter", :action => "hello_world", :params => { :person => "david" }
9+
end
10+
end
11+
12+
class GreeterController < ActionController::Base
13+
def hello_world
14+
render :text => "#{params[:person]} says, Hello World!"
15+
end
16+
end
17+
18+
The same can be done in a view to do a partial rendering:
19+
20+
Let's see a greeting:
21+
<%= render_component :controller => "greeter", :action => "hello_world" %>
22+
23+
It is also possible to specify the controller as a class constant, bypassing the inflector
24+
code to compute the controller class at runtime:
25+
26+
<%= render_component :controller => GreeterController, :action => "hello_world" %>
27+
28+
== When to use components
29+
30+
Components should be used with care. They're significantly slower than simply splitting reusable parts into partials and
31+
conceptually more complicated. Don't use components as a way of separating concerns inside a single application. Instead,
32+
reserve components to those rare cases where you truly have reusable view and controller elements that can be employed
33+
across many applications at once.
34+
35+
So to repeat: Components are a special-purpose approach that can often be replaced with better use of partials and filters.
36+
37+
Copyright (c) 2007 David Heinemeier Hansson, released under the MIT license

Rakefile

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
require 'rake'
2+
require 'rake/testtask'
3+
require 'rake/rdoctask'
4+
5+
desc 'Default: run unit tests.'
6+
task :default => :test
7+
8+
desc 'Test the components plugin.'
9+
Rake::TestTask.new(:test) do |t|
10+
t.libs << 'lib'
11+
t.pattern = 'test/**/*_test.rb'
12+
t.verbose = true
13+
end
14+
15+
desc 'Generate documentation for the components plugin.'
16+
Rake::RDocTask.new(:rdoc) do |rdoc|
17+
rdoc.rdoc_dir = 'rdoc'
18+
rdoc.title = 'Components'
19+
rdoc.options << '--line-numbers' << '--inline-source'
20+
rdoc.rdoc_files.include('README')
21+
rdoc.rdoc_files.include('lib/**/*.rb')
22+
end

init.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
require 'components'
2+
ActionController::Base.send :include, Components

lib/components.rb

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
module Components
2+
def self.included(base) #:nodoc:
3+
base.class_eval do
4+
include InstanceMethods
5+
extend ClassMethods
6+
helper HelperMethods
7+
8+
# If this controller was instantiated to process a component request,
9+
# +parent_controller+ points to the instantiator of this controller.
10+
attr_accessor :parent_controller
11+
12+
alias_method_chain :process_cleanup, :render_component
13+
alias_method_chain :set_session_options, :render_component
14+
alias_method_chain :flash, :render_component
15+
alias_method_chain :assign_shortcuts, :render_component
16+
alias_method_chain :send_response, :render_component
17+
18+
alias_method :component_request?, :parent_controller
19+
end
20+
end
21+
22+
module ClassMethods
23+
# Track parent controller to identify component requests
24+
def process_with_components(request, response, parent_controller = nil) #:nodoc:
25+
controller = new
26+
controller.parent_controller = parent_controller
27+
controller.process(request, response)
28+
end
29+
end
30+
31+
module HelperMethods
32+
def render_component(options)
33+
@controller.send!(:render_component_as_string, options)
34+
end
35+
end
36+
37+
module InstanceMethods
38+
# Extracts the action_name from the request parameters and performs that action.
39+
def process_with_components(request, response, method = :perform_action, *arguments) #:nodoc:
40+
flash.discard if component_request?
41+
process_without_components(request, response, method, *arguments)
42+
end
43+
44+
def send_response_with_render_component
45+
response.prepare! unless component_request?
46+
response
47+
end
48+
49+
protected
50+
# Renders the component specified as the response for the current method
51+
def render_component(options) #:doc:
52+
component_logging(options) do
53+
render_for_text(component_response(options, true).body, response.headers["Status"])
54+
end
55+
end
56+
57+
# Returns the component response as a string
58+
def render_component_as_string(options) #:doc:
59+
component_logging(options) do
60+
response = component_response(options, false)
61+
62+
if redirected = response.redirected_to
63+
render_component_as_string(redirected)
64+
else
65+
response.body
66+
end
67+
end
68+
end
69+
70+
def flash_with_render_component(refresh = false) #:nodoc:
71+
if !defined?(@_flash) || refresh
72+
@_flash =
73+
if defined?(@parent_controller)
74+
@parent_controller.flash
75+
else
76+
flash_without_render_component
77+
end
78+
end
79+
@_flash
80+
end
81+
82+
private
83+
def component_response(options, reuse_response)
84+
klass = component_class(options)
85+
request = request_for_component(klass.controller_name, options)
86+
new_response = reuse_response ? response : response.dup
87+
88+
klass.process_with_components(request, new_response, self)
89+
end
90+
91+
# determine the controller class for the component request
92+
def component_class(options)
93+
if controller = options[:controller]
94+
controller.is_a?(Class) ? controller : "#{controller.camelize}Controller".constantize
95+
else
96+
self.class
97+
end
98+
end
99+
100+
# Create a new request object based on the current request.
101+
# The new request inherits the session from the current request,
102+
# bypassing any session options set for the component controller's class
103+
def request_for_component(controller_name, options)
104+
new_request = request.dup
105+
new_request.session = request.session
106+
107+
new_request.instance_variable_set(
108+
:@parameters,
109+
(options[:params] || {}).with_indifferent_access.update(
110+
"controller" => controller_name, "action" => options[:action], "id" => options[:id]
111+
)
112+
)
113+
114+
new_request
115+
end
116+
117+
def component_logging(options)
118+
if logger
119+
logger.info "Start rendering component (#{options.inspect}): "
120+
result = yield
121+
logger.info "\n\nEnd of component rendering"
122+
result
123+
else
124+
yield
125+
end
126+
end
127+
128+
def set_session_options_with_render_component(request)
129+
set_session_options_without_render_component(request) unless component_request?
130+
end
131+
132+
def process_cleanup_with_render_component
133+
process_cleanup_without_render_component unless component_request?
134+
end
135+
136+
def assign_shortcuts_with_render_component(request, response)
137+
assign_shortcuts_without_flash(request, response)
138+
flash(:refresh)
139+
flash.sweep if @_session && !component_request?
140+
end
141+
end
142+
end

test/abstract_unit.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# [TODO] Replace with requiring rails gem after we release 2.2
2+
$: << '/Users/lifo/commit-rails/rails/actionpack/lib'
3+
4+
require 'test/unit'
5+
require 'action_controller'
6+
require 'action_controller/test_process'
7+
ActionController::Routing::Routes.reload rescue nil
8+
9+
$: << File.dirname(__FILE__) + "/../lib"
10+
require File.dirname(__FILE__) + "/../init"

test/components_test.rb

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
require File.dirname(__FILE__) + '/abstract_unit'
2+
3+
class CallerController < ActionController::Base
4+
def calling_from_controller
5+
render_component(:controller => "callee", :action => "being_called")
6+
end
7+
8+
def calling_from_controller_with_params
9+
render_component(:controller => "callee", :action => "being_called", :params => { "name" => "David" })
10+
end
11+
12+
def calling_from_controller_with_different_status_code
13+
render_component(:controller => "callee", :action => "blowing_up")
14+
end
15+
16+
def calling_from_template
17+
render :inline => "Ring, ring: <%= render_component(:controller => 'callee', :action => 'being_called') %>"
18+
end
19+
20+
def internal_caller
21+
render :inline => "Are you there? <%= render_component(:action => 'internal_callee') %>"
22+
end
23+
24+
def internal_callee
25+
render :text => "Yes, ma'am"
26+
end
27+
28+
def set_flash
29+
render_component(:controller => "callee", :action => "set_flash")
30+
end
31+
32+
def use_flash
33+
render_component(:controller => "callee", :action => "use_flash")
34+
end
35+
36+
def calling_redirected
37+
render_component(:controller => "callee", :action => "redirected")
38+
end
39+
40+
def calling_redirected_as_string
41+
render :inline => "<%= render_component(:controller => 'callee', :action => 'redirected') %>"
42+
end
43+
44+
def rescue_action(e) raise end
45+
end
46+
47+
class CalleeController < ActionController::Base
48+
def being_called
49+
render :text => "#{params[:name] || "Lady"} of the House, speaking"
50+
end
51+
52+
def blowing_up
53+
render :text => "It's game over, man, just game over, man!", :status => 500
54+
end
55+
56+
def set_flash
57+
flash[:notice] = 'My stoney baby'
58+
render :text => 'flash is set'
59+
end
60+
61+
def use_flash
62+
render :text => flash[:notice] || 'no flash'
63+
end
64+
65+
def redirected
66+
redirect_to :controller => "callee", :action => "being_called"
67+
end
68+
69+
def rescue_action(e) raise end
70+
end
71+
72+
class ComponentsTest < Test::Unit::TestCase
73+
def setup
74+
@controller = CallerController.new
75+
@request = ActionController::TestRequest.new
76+
@response = ActionController::TestResponse.new
77+
end
78+
79+
def test_calling_from_controller
80+
get :calling_from_controller
81+
assert_equal "Lady of the House, speaking", @response.body
82+
end
83+
84+
def test_calling_from_controller_with_params
85+
get :calling_from_controller_with_params
86+
assert_equal "David of the House, speaking", @response.body
87+
end
88+
89+
def test_calling_from_controller_with_different_status_code
90+
get :calling_from_controller_with_different_status_code
91+
assert_equal 500, @response.response_code
92+
end
93+
94+
def test_calling_from_template
95+
get :calling_from_template
96+
assert_equal "Ring, ring: Lady of the House, speaking", @response.body
97+
end
98+
99+
def test_etag_is_set_for_parent_template_when_calling_from_template
100+
get :calling_from_template
101+
expected_etag = etag_for("Ring, ring: Lady of the House, speaking")
102+
assert_equal expected_etag, @response.headers['ETag']
103+
end
104+
105+
def test_internal_calling
106+
get :internal_caller
107+
assert_equal "Are you there? Yes, ma'am", @response.body
108+
end
109+
110+
def test_flash
111+
get :set_flash
112+
assert_equal 'My stoney baby', flash[:notice]
113+
get :use_flash
114+
assert_equal 'My stoney baby', @response.body
115+
get :use_flash
116+
assert_equal 'no flash', @response.body
117+
end
118+
119+
def test_component_redirect_redirects
120+
get :calling_redirected
121+
122+
assert_redirected_to :controller=>"callee", :action => "being_called"
123+
end
124+
125+
def test_component_multiple_redirect_redirects
126+
test_component_redirect_redirects
127+
test_internal_calling
128+
end
129+
130+
def test_component_as_string_redirect_renders_redirected_action
131+
get :calling_redirected_as_string
132+
133+
assert_equal "Lady of the House, speaking", @response.body
134+
end
135+
136+
protected
137+
def etag_for(text)
138+
%("#{Digest::MD5.hexdigest(text)}")
139+
end
140+
end

0 commit comments

Comments
 (0)