Skip to content

Commit a69367a

Browse files
author
Robert Mosolgo
committed
Merge pull request #346 from rmosolgo/configurable-view-helper
Configurable view helper
2 parents 8696f50 + 0c77042 commit a69367a

9 files changed

+193
-66
lines changed

lib/react/rails.rb

+2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
require 'react/rails/asset_variant'
22
require 'react/rails/engine'
33
require 'react/rails/railtie'
4+
require 'react/rails/render_middleware'
45
require 'react/rails/version'
6+
require 'react/rails/component_mount'
57
require 'react/rails/view_helper'
68
require 'react/rails/controller_renderer'

lib/react/rails/component_mount.rb

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
module React
2+
module Rails
3+
# This is the default view helper implementation.
4+
# It just inserts HTML into the DOM (see {#react_component}).
5+
#
6+
# You can extend this class or provide your own implementation
7+
# by assigning it to `config.react.view_helper_implementation`.
8+
class ComponentMount
9+
include ActionView::Helpers::TagHelper
10+
include ActionView::Helpers::TextHelper
11+
attr_accessor :output_buffer
12+
13+
# RenderMiddleware calls these hooks
14+
# You can use them in custom helper implementations
15+
def setup(env)
16+
end
17+
18+
def teardown(env)
19+
end
20+
21+
# Render a UJS-type HTML tag annotated with data attributes, which
22+
# are used by react_ujs to actually instantiate the React component
23+
# on the client.
24+
def react_component(name, props = {}, options = {}, &block)
25+
options = {:tag => options} if options.is_a?(Symbol)
26+
27+
prerender_options = options[:prerender]
28+
if prerender_options
29+
block = Proc.new{ concat React::ServerRendering.render(name, props, prerender_options) }
30+
end
31+
32+
html_options = options.reverse_merge(:data => {})
33+
html_options[:data].tap do |data|
34+
data[:react_class] = name
35+
data[:react_props] = (props.is_a?(String) ? props : props.to_json)
36+
end
37+
html_tag = html_options[:tag] || :div
38+
39+
# remove internally used properties so they aren't rendered to DOM
40+
html_options.except!(:tag, :prerender)
41+
42+
content_tag(html_tag, '', html_options, &block)
43+
end
44+
end
45+
end
46+
end

lib/react/rails/controller_renderer.rb

+3-2
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ class React::Rails::ControllerRenderer
55

66
attr_accessor :output_buffer
77

8-
def self.call(*args, &block)
9-
new.call(*args, &block)
8+
attr_reader :request
9+
def initialize(options={})
10+
@request = options[:request]
1011
end
1112

1213
def call(name, options, &block)

lib/react/rails/railtie.rb

+7-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ module React
44
module Rails
55
class Railtie < ::Rails::Railtie
66
config.react = ActiveSupport::OrderedOptions.new
7-
87
# Sensible defaults. Can be overridden in application.rb
98
config.react.variant = (::Rails.env.production? ? :production : :development)
109
config.react.addons = false
@@ -15,6 +14,8 @@ class Railtie < ::Rails::Railtie
1514
config.react.server_renderer_timeout = 20 # seconds
1615
config.react.server_renderer = nil # defaults to SprocketsRenderer
1716
config.react.server_renderer_options = {} # SprocketsRenderer provides defaults
17+
# View helper implementation:
18+
config.react.view_helper_implementation = nil # Defaults to ComponentMount
1819

1920
# Watch .jsx files for changes in dev, so we can reload the JS VMs with the new JS code.
2021
initializer "react_rails.add_watchable_files", group: :all do |app|
@@ -23,18 +24,22 @@ class Railtie < ::Rails::Railtie
2324

2425
# Include the react-rails view helper lazily
2526
initializer "react_rails.setup_view_helpers", group: :all do |app|
27+
app.config.middleware.use(::React::Rails::RenderMiddleware)
2628
app.config.react.jsx_transformer_class ||= React::JSX::DEFAULT_TRANSFORMER
2729
React::JSX.transformer_class = app.config.react.jsx_transformer_class
2830
React::JSX.transform_options = app.config.react.jsx_transform_options
2931

32+
app.config.react.view_helper_implementation ||= React::Rails::ComponentMount
33+
React::Rails::ViewHelper.helper_implementation_class = app.config.react.view_helper_implementation
3034
ActiveSupport.on_load(:action_view) do
3135
include ::React::Rails::ViewHelper
3236
end
3337
end
3438

3539
initializer "react_rails.add_component_renderer", group: :all do |app|
3640
ActionController::Renderers.add :component do |component_name, options|
37-
html = ::React::Rails::ControllerRenderer.call(component_name, options)
41+
renderer = ::React::Rails::ControllerRenderer.new(request: request)
42+
html = renderer.call(component_name, options)
3843
render_options = options.merge(inline: html)
3944
render(render_options)
4045
end

lib/react/rails/render_middleware.rb

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
module React
2+
module Rails
3+
class RenderMiddleware
4+
HELPER_IMPLEMENTATION_KEY = "react_rails.view_helper_implementation"
5+
def initialize(app)
6+
@app = app
7+
end
8+
9+
def call(env)
10+
new_helper = React::Rails::ViewHelper.helper_implementation_class.new
11+
new_helper.setup(env)
12+
env[HELPER_IMPLEMENTATION_KEY] = new_helper
13+
response = @app.call(env)
14+
new_helper.teardown(env)
15+
response
16+
end
17+
end
18+
end
19+
end

lib/react/rails/view_helper.rb

+11-21
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,18 @@
11
module React
22
module Rails
33
module ViewHelper
4-
# Render a UJS-type HTML tag annotated with data attributes, which
5-
# are used by react_ujs to actually instantiate the React component
6-
# on the client.
7-
def react_component(name, props = {}, options = {}, &block)
8-
options = {:tag => options} if options.is_a?(Symbol)
4+
# This class will be used for inserting tags into HTML.
5+
# It should implement:
6+
# - #setup(env)
7+
# - #teardown(env)
8+
# - #react_component(name, props, options &block)
9+
# The default is {React::Rails::ComponentMount}
10+
mattr_accessor :helper_implementation_class
911

10-
prerender_options = options[:prerender]
11-
if prerender_options
12-
block = Proc.new{ concat React::ServerRendering.render(name, props, prerender_options) }
13-
end
14-
15-
html_options = options.reverse_merge(:data => {})
16-
html_options[:data].tap do |data|
17-
data[:react_class] = name
18-
data[:react_props] = (props.is_a?(String) ? props : props.to_json)
19-
end
20-
html_tag = html_options[:tag] || :div
21-
22-
# remove internally used properties so they aren't rendered to DOM
23-
html_options.except!(:tag, :prerender)
24-
25-
content_tag(html_tag, '', html_options, &block)
12+
def react_component(*args, &block)
13+
impl_key = React::Rails::RenderMiddleware::HELPER_IMPLEMENTATION_KEY
14+
helper_obj = request.env[impl_key]
15+
helper_obj.react_component(*args, &block)
2616
end
2717
end
2818
end
+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
require 'test_helper'
2+
3+
class ComponentMountTest < ActionDispatch::IntegrationTest
4+
setup do
5+
@helper = React::Rails::ComponentMount.new
6+
end
7+
8+
test '#react_component accepts React props' do
9+
html = @helper.react_component('Foo', {bar: 'value'})
10+
expected_props = %w(data-react-class="Foo" data-react-props="{&quot;bar&quot;:&quot;value&quot;}")
11+
expected_props.each do |segment|
12+
assert html.include?(segment)
13+
end
14+
end
15+
16+
test '#react_component accepts jbuilder-based strings as properties' do
17+
jbuilder_json = Jbuilder.new do |json|
18+
json.bar 'value'
19+
end.target!
20+
21+
html = @helper.react_component('Foo', jbuilder_json)
22+
expected_props = %w(data-react-class="Foo" data-react-props="{&quot;bar&quot;:&quot;value&quot;}")
23+
expected_props.each do |segment|
24+
assert html.include?(segment), "expected #{html} to include #{segment}"
25+
end
26+
end
27+
28+
test '#react_component accepts string props with prerender: true' do
29+
html = @helper.react_component('Todo', {todo: 'render on the server'}.to_json, prerender: true)
30+
assert(html.include?('data-react-class="Todo"'), "it includes attrs for UJS")
31+
assert(html.include?('>render on the server</li>'), "it includes rendered HTML")
32+
assert(html.include?('data-reactid'), "it includes React properties")
33+
end
34+
35+
test '#react_component passes :static to SprocketsRenderer' do
36+
html = @helper.react_component('Todo', {todo: 'render on the server'}.to_json, prerender: :static)
37+
assert(html.include?('>render on the server</li>'), "it includes rendered HTML")
38+
assert(!html.include?('data-reactid'), "it DOESNT INCLUDE React properties")
39+
end
40+
41+
test '#react_component accepts HTML options and HTML tag' do
42+
assert @helper.react_component('Foo', {}, :span).match(/<span\s.*><\/span>/)
43+
44+
html = @helper.react_component('Foo', {}, {class: 'test', tag: :span, data: {foo: 1}})
45+
assert html.match(/<span\s.*><\/span>/)
46+
assert html.include?('class="test"')
47+
assert html.include?('data-foo="1"')
48+
end
49+
end
+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
require 'test_helper'
2+
3+
# This helper implementation just counts the number of
4+
# calls to `react_component`
5+
class DummyHelperImplementation
6+
attr_reader :events
7+
def initialize
8+
@events = []
9+
end
10+
11+
def setup(env)
12+
@events << :setup
13+
end
14+
15+
def teardown(env)
16+
@events << :teardown
17+
end
18+
19+
def react_component(*args)
20+
@events << :react_component
21+
end
22+
end
23+
24+
class RenderMiddlewareTest < ActionDispatch::IntegrationTest
25+
impl_key = React::Rails::RenderMiddleware::HELPER_IMPLEMENTATION_KEY
26+
27+
def setup
28+
@previous_helper_implementation = React::Rails::ViewHelper.helper_implementation_class
29+
React::Rails::ViewHelper.helper_implementation_class = DummyHelperImplementation
30+
end
31+
32+
def teardown
33+
React::Rails::ViewHelper.helper_implementation_class = @previous_helper_implementation
34+
end
35+
36+
test "it creates a helper object and puts it in the request env" do
37+
get '/pages/1'
38+
helper_obj = request.env[impl_key]
39+
assert(helper_obj.is_a?(DummyHelperImplementation), "It uses the view helper implementation class")
40+
end
41+
42+
test "it calls setup and teardown methods" do
43+
get '/pages/1'
44+
helper_obj = request.env[impl_key]
45+
lifecycle_steps = [:setup, :react_component, :teardown]
46+
assert_equal(lifecycle_steps, helper_obj.events)
47+
end
48+
49+
test "there's a new helper object for every request" do
50+
get '/pages/1'
51+
first_helper = request.env[impl_key]
52+
get '/pages/1'
53+
second_helper = request.env[impl_key]
54+
assert(first_helper != second_helper, "The helper for the second request is brand new")
55+
end
56+
end

test/react/rails/view_helper_test.rb

-41
Original file line numberDiff line numberDiff line change
@@ -18,50 +18,9 @@ class ViewHelperTest < ActionDispatch::IntegrationTest
1818
include Capybara::DSL
1919

2020
setup do
21-
@helper = ActionView::Base.new.extend(React::Rails::ViewHelper)
2221
Capybara.current_driver = Capybara.javascript_driver
2322
end
2423

25-
test 'react_component accepts React props' do
26-
html = @helper.react_component('Foo', {bar: 'value'})
27-
%w(data-react-class="Foo" data-react-props="{&quot;bar&quot;:&quot;value&quot;}").each do |segment|
28-
assert html.include?(segment)
29-
end
30-
end
31-
32-
test 'react_component accepts jbuilder-based strings as properties' do
33-
jbuilder_json = Jbuilder.new do |json|
34-
json.bar 'value'
35-
end.target!
36-
37-
html = @helper.react_component('Foo', jbuilder_json)
38-
%w(data-react-class="Foo" data-react-props="{&quot;bar&quot;:&quot;value&quot;}").each do |segment|
39-
assert html.include?(segment), "expected #{html} to include #{segment}"
40-
end
41-
end
42-
43-
test 'react_component accepts string props with prerender: true' do
44-
html = @helper.react_component('Todo', {todo: 'render on the server'}.to_json, prerender: true)
45-
assert(html.include?('data-react-class="Todo"'), "it includes attrs for UJS")
46-
assert(html.include?('>render on the server</li>'), "it includes rendered HTML")
47-
assert(html.include?('data-reactid'), "it includes React properties")
48-
end
49-
50-
test 'react_component passes :static to SprocketsRenderer' do
51-
html = @helper.react_component('Todo', {todo: 'render on the server'}.to_json, prerender: :static)
52-
assert(html.include?('>render on the server</li>'), "it includes rendered HTML")
53-
assert(!html.include?('data-reactid'), "it DOESNT INCLUDE React properties")
54-
end
55-
56-
test 'react_component accepts HTML options and HTML tag' do
57-
assert @helper.react_component('Foo', {}, :span).match(/<span\s.*><\/span>/)
58-
59-
html = @helper.react_component('Foo', {}, {:class => 'test', :tag => :span, :data => {:foo => 1}})
60-
assert html.match(/<span\s.*><\/span>/)
61-
assert html.include?('class="test"')
62-
assert html.include?('data-foo="1"')
63-
end
64-
6524
test 'ujs object present on the global React object and has our methods' do
6625
visit '/pages/1'
6726
assert page.has_content?('Hello Bob')

0 commit comments

Comments
 (0)