Skip to content

Configurable view helper #346

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Sep 4, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions lib/react/rails.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
require 'react/rails/asset_variant'
require 'react/rails/engine'
require 'react/rails/railtie'
require 'react/rails/render_middleware'
require 'react/rails/version'
require 'react/rails/component_mount'
require 'react/rails/view_helper'
require 'react/rails/controller_renderer'
46 changes: 46 additions & 0 deletions lib/react/rails/component_mount.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
module React
module Rails
# This is the default view helper implementation.
# It just inserts HTML into the DOM (see {#react_component}).
#
# You can extend this class or provide your own implementation
# by assigning it to `config.react.view_helper_implementation`.
class ComponentMount
include ActionView::Helpers::TagHelper
include ActionView::Helpers::TextHelper
attr_accessor :output_buffer

# RenderMiddleware calls these hooks
# You can use them in custom helper implementations
def setup(env)
end

def teardown(env)
end

# Render a UJS-type HTML tag annotated with data attributes, which
# are used by react_ujs to actually instantiate the React component
# on the client.
def react_component(name, props = {}, options = {}, &block)
options = {:tag => options} if options.is_a?(Symbol)

prerender_options = options[:prerender]
if prerender_options
block = Proc.new{ concat React::ServerRendering.render(name, props, prerender_options) }
end

html_options = options.reverse_merge(:data => {})
html_options[:data].tap do |data|
data[:react_class] = name
data[:react_props] = (props.is_a?(String) ? props : props.to_json)
end
html_tag = html_options[:tag] || :div

# remove internally used properties so they aren't rendered to DOM
html_options.except!(:tag, :prerender)

content_tag(html_tag, '', html_options, &block)
end
end
end
end
5 changes: 3 additions & 2 deletions lib/react/rails/controller_renderer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ class React::Rails::ControllerRenderer

attr_accessor :output_buffer

def self.call(*args, &block)
new.call(*args, &block)
attr_reader :request
def initialize(options={})
@request = options[:request]
end

def call(name, options, &block)
Expand Down
9 changes: 7 additions & 2 deletions lib/react/rails/railtie.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ module React
module Rails
class Railtie < ::Rails::Railtie
config.react = ActiveSupport::OrderedOptions.new

# Sensible defaults. Can be overridden in application.rb
config.react.variant = (::Rails.env.production? ? :production : :development)
config.react.addons = false
Expand All @@ -15,6 +14,8 @@ class Railtie < ::Rails::Railtie
config.react.server_renderer_timeout = 20 # seconds
config.react.server_renderer = nil # defaults to SprocketsRenderer
config.react.server_renderer_options = {} # SprocketsRenderer provides defaults
# View helper implementation:
config.react.view_helper_implementation = nil # Defaults to ComponentMount

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

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

app.config.react.view_helper_implementation ||= React::Rails::ComponentMount
React::Rails::ViewHelper.helper_implementation_class = app.config.react.view_helper_implementation
ActiveSupport.on_load(:action_view) do
include ::React::Rails::ViewHelper
end
end

initializer "react_rails.add_component_renderer", group: :all do |app|
ActionController::Renderers.add :component do |component_name, options|
html = ::React::Rails::ControllerRenderer.call(component_name, options)
renderer = ::React::Rails::ControllerRenderer.new(request: request)
html = renderer.call(component_name, options)
render_options = options.merge(inline: html)
render(render_options)
end
Expand Down
19 changes: 19 additions & 0 deletions lib/react/rails/render_middleware.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
module React
module Rails
class RenderMiddleware
HELPER_IMPLEMENTATION_KEY = "react_rails.view_helper_implementation"
def initialize(app)
@app = app
end

def call(env)
new_helper = React::Rails::ViewHelper.helper_implementation_class.new
new_helper.setup(env)
env[HELPER_IMPLEMENTATION_KEY] = new_helper
response = @app.call(env)
new_helper.teardown(env)
response
end
end
end
end
32 changes: 11 additions & 21 deletions lib/react/rails/view_helper.rb
Original file line number Diff line number Diff line change
@@ -1,28 +1,18 @@
module React
module Rails
module ViewHelper
# Render a UJS-type HTML tag annotated with data attributes, which
# are used by react_ujs to actually instantiate the React component
# on the client.
def react_component(name, props = {}, options = {}, &block)
options = {:tag => options} if options.is_a?(Symbol)
# This class will be used for inserting tags into HTML.
# It should implement:
# - #setup(env)
# - #teardown(env)
# - #react_component(name, props, options &block)
# The default is {React::Rails::ComponentMount}
mattr_accessor :helper_implementation_class

prerender_options = options[:prerender]
if prerender_options
block = Proc.new{ concat React::ServerRendering.render(name, props, prerender_options) }
end

html_options = options.reverse_merge(:data => {})
html_options[:data].tap do |data|
data[:react_class] = name
data[:react_props] = (props.is_a?(String) ? props : props.to_json)
end
html_tag = html_options[:tag] || :div

# remove internally used properties so they aren't rendered to DOM
html_options.except!(:tag, :prerender)

content_tag(html_tag, '', html_options, &block)
def react_component(*args, &block)
impl_key = React::Rails::RenderMiddleware::HELPER_IMPLEMENTATION_KEY
helper_obj = request.env[impl_key]
helper_obj.react_component(*args, &block)
end
end
end
Expand Down
49 changes: 49 additions & 0 deletions test/react/rails/component_mount_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
require 'test_helper'

class ComponentMountTest < ActionDispatch::IntegrationTest
setup do
@helper = React::Rails::ComponentMount.new
end

test '#react_component accepts React props' do
html = @helper.react_component('Foo', {bar: 'value'})
expected_props = %w(data-react-class="Foo" data-react-props="{&quot;bar&quot;:&quot;value&quot;}")
expected_props.each do |segment|
assert html.include?(segment)
end
end

test '#react_component accepts jbuilder-based strings as properties' do
jbuilder_json = Jbuilder.new do |json|
json.bar 'value'
end.target!

html = @helper.react_component('Foo', jbuilder_json)
expected_props = %w(data-react-class="Foo" data-react-props="{&quot;bar&quot;:&quot;value&quot;}")
expected_props.each do |segment|
assert html.include?(segment), "expected #{html} to include #{segment}"
end
end

test '#react_component accepts string props with prerender: true' do
html = @helper.react_component('Todo', {todo: 'render on the server'}.to_json, prerender: true)
assert(html.include?('data-react-class="Todo"'), "it includes attrs for UJS")
assert(html.include?('>render on the server</li>'), "it includes rendered HTML")
assert(html.include?('data-reactid'), "it includes React properties")
end

test '#react_component passes :static to SprocketsRenderer' do
html = @helper.react_component('Todo', {todo: 'render on the server'}.to_json, prerender: :static)
assert(html.include?('>render on the server</li>'), "it includes rendered HTML")
assert(!html.include?('data-reactid'), "it DOESNT INCLUDE React properties")
end

test '#react_component accepts HTML options and HTML tag' do
assert @helper.react_component('Foo', {}, :span).match(/<span\s.*><\/span>/)

html = @helper.react_component('Foo', {}, {class: 'test', tag: :span, data: {foo: 1}})
assert html.match(/<span\s.*><\/span>/)
assert html.include?('class="test"')
assert html.include?('data-foo="1"')
end
end
56 changes: 56 additions & 0 deletions test/react/rails/render_middleware_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
require 'test_helper'

# This helper implementation just counts the number of
# calls to `react_component`
class DummyHelperImplementation
attr_reader :events
def initialize
@events = []
end

def setup(env)
@events << :setup
end

def teardown(env)
@events << :teardown
end

def react_component(*args)
@events << :react_component
end
end

class RenderMiddlewareTest < ActionDispatch::IntegrationTest
impl_key = React::Rails::RenderMiddleware::HELPER_IMPLEMENTATION_KEY

def setup
@previous_helper_implementation = React::Rails::ViewHelper.helper_implementation_class
React::Rails::ViewHelper.helper_implementation_class = DummyHelperImplementation
end

def teardown
React::Rails::ViewHelper.helper_implementation_class = @previous_helper_implementation
end

test "it creates a helper object and puts it in the request env" do
get '/pages/1'
helper_obj = request.env[impl_key]
assert(helper_obj.is_a?(DummyHelperImplementation), "It uses the view helper implementation class")
end

test "it calls setup and teardown methods" do
get '/pages/1'
helper_obj = request.env[impl_key]
lifecycle_steps = [:setup, :react_component, :teardown]
assert_equal(lifecycle_steps, helper_obj.events)
end

test "there's a new helper object for every request" do
get '/pages/1'
first_helper = request.env[impl_key]
get '/pages/1'
second_helper = request.env[impl_key]
assert(first_helper != second_helper, "The helper for the second request is brand new")
end
end
41 changes: 0 additions & 41 deletions test/react/rails/view_helper_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,50 +18,9 @@ class ViewHelperTest < ActionDispatch::IntegrationTest
include Capybara::DSL

setup do
@helper = ActionView::Base.new.extend(React::Rails::ViewHelper)
Capybara.current_driver = Capybara.javascript_driver
end

test 'react_component accepts React props' do
html = @helper.react_component('Foo', {bar: 'value'})
%w(data-react-class="Foo" data-react-props="{&quot;bar&quot;:&quot;value&quot;}").each do |segment|
assert html.include?(segment)
end
end

test 'react_component accepts jbuilder-based strings as properties' do
jbuilder_json = Jbuilder.new do |json|
json.bar 'value'
end.target!

html = @helper.react_component('Foo', jbuilder_json)
%w(data-react-class="Foo" data-react-props="{&quot;bar&quot;:&quot;value&quot;}").each do |segment|
assert html.include?(segment), "expected #{html} to include #{segment}"
end
end

test 'react_component accepts string props with prerender: true' do
html = @helper.react_component('Todo', {todo: 'render on the server'}.to_json, prerender: true)
assert(html.include?('data-react-class="Todo"'), "it includes attrs for UJS")
assert(html.include?('>render on the server</li>'), "it includes rendered HTML")
assert(html.include?('data-reactid'), "it includes React properties")
end

test 'react_component passes :static to SprocketsRenderer' do
html = @helper.react_component('Todo', {todo: 'render on the server'}.to_json, prerender: :static)
assert(html.include?('>render on the server</li>'), "it includes rendered HTML")
assert(!html.include?('data-reactid'), "it DOESNT INCLUDE React properties")
end

test 'react_component accepts HTML options and HTML tag' do
assert @helper.react_component('Foo', {}, :span).match(/<span\s.*><\/span>/)

html = @helper.react_component('Foo', {}, {:class => 'test', :tag => :span, :data => {:foo => 1}})
assert html.match(/<span\s.*><\/span>/)
assert html.include?('class="test"')
assert html.include?('data-foo="1"')
end

test 'ujs object present on the global React object and has our methods' do
visit '/pages/1'
assert page.has_content?('Hello Bob')
Expand Down