Skip to content

Inline props operations improvement #306

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

Closed
wants to merge 10 commits into from
Closed
12 changes: 11 additions & 1 deletion lib/assets/javascripts/react_ujs.js.erb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
window.ReactRailsUJS = {
CLASS_NAME_ATTR: 'data-react-class',
PROPS_ATTR: 'data-react-props',
PROPS_ID_ATTR: 'data-react-props-id',
RAILS_ENV_DEVELOPMENT: <%= Rails.env == "development" %>,
// helper method for the mount and unmount methods to find the
// `data-react-class` DOM elements
Expand All @@ -27,13 +28,22 @@
var nodes = window.ReactRailsUJS.findDOMNodes();

for (var i = 0; i < nodes.length; ++i) {
var propsId, propsElement, propsJson;
var node = nodes[i];
var className = node.getAttribute(window.ReactRailsUJS.CLASS_NAME_ATTR);

// Assume className is simple and can be found at top-level (window).
// Fallback to eval to handle cases like 'My.React.ComponentName'.
var constructor = window[className] || eval.call(window, className);
var propsJson = node.getAttribute(window.ReactRailsUJS.PROPS_ATTR);

propsId = node.getAttribute(window.ReactRailsUJS.PROPS_ID_ATTR);
if (propsId != null) {
propsElement = document.getElementById(propsId);
propsJson = propsElement && propsElement.text;
} else {
propsJson = node.getAttribute(window.ReactRailsUJS.PROPS_ATTR);
}

var props = propsJson && JSON.parse(propsJson);

React.render(React.createElement(constructor, props), node);
Expand Down
1 change: 1 addition & 0 deletions lib/react/rails/railtie.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ class Railtie < ::Rails::Railtie
config.react.variant = (::Rails.env.production? ? :production : :development)
config.react.addons = false
config.react.jsx_transform_options = {}

# Server-side rendering
config.react.max_renderers = 10
config.react.timeout = 20 #seconds
Expand Down
37 changes: 34 additions & 3 deletions lib/react/rails/view_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,49 @@ module ViewHelper
def react_component(name, args = {}, options = {}, &block)
options = {:tag => options} if options.is_a?(Symbol)
block = Proc.new{concat React::Renderer.render(name, args)} if options[:prerender]
separate_props = options.delete :separate_props
move_separate_props_out = options.delete :move_separate_props_out

html_options = options.reverse_merge(:data => {})
html_options[:data].tap do |data|
data[:react_class] = name
data[:react_props] = React::Renderer.react_props(args) unless args.empty?
next if args.empty?
if separate_props
data[:react_props_id] = add_react_props args, move_separate_props_out
else
data[:react_props] = React::Renderer.react_props args
end
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)

result = content_tag(html_tag, '', html_options, &block)
result += render_react_props html_options[:data][:react_props_id] if separate_props && !move_separate_props_out
result
end

# Add properties for component and return element id.
#
def add_react_props(props={}, move_out=false)
return if props.empty?
props_id = SecureRandom.base64
content_key = "react_props"
content_key += "_#{props_id}" unless move_out
content_for content_key do
content_tag :script, type: 'text/json', id: props_id do
raw React::Renderer.react_props props
end
end
props_id
end

# Render script tag with JSON props. Should be placed at the end of body
# in order to speedup page rendering.
#
def render_react_props(element_id=nil)
element_id.nil? && content_for('react_props') || content_for("react_props_#{element_id}")
end

end
Expand Down
2 changes: 2 additions & 0 deletions test/dummy/app/controllers/separate_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
class SeparateController < ServerController
end
4 changes: 4 additions & 0 deletions test/dummy/app/views/layouts/application.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,9 @@

<%= yield %>

<div id="dummy"></div>

<%= render_react_props %>

</body>
</html>
1 change: 1 addition & 0 deletions test/dummy/app/views/separate/show.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<%= react_component "TodoList", {:todos => @todos}, :prerender => true, :separate_props => true, :move_separate_props_out => params[:move_separate_props_out] %>
1 change: 1 addition & 0 deletions test/dummy/config/routes.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
Dummy::Application.routes.draw do
resources :pages, :only => [:show]
resources :server, :only => [:show]
resources :separate, :only => [:show]
end
34 changes: 29 additions & 5 deletions test/view_helper_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,25 @@ class ViewHelperTest < ActionDispatch::IntegrationTest
assert html.include?('class="test"')
assert html.include?('data-foo="1"')
end


test 'react_component can render separate props inline' do
html = @helper.react_component "TodoList", {:todos => %w(todo1 todo2, todo3)}, :prerender => true, :separate_props => true
assert_match /data-react-class=\"TodoList\"/, html
assert_match /{"todos":\["todo1","todo2,","todo3"\]}<\/script>$/, html
end

test 'react_component can render separate props moved to any place in DOM' do
html = @helper.react_component "TodoList", {:todos => %w(todo1 todo2, todo3)},
:prerender => true,
:separate_props => true,
:move_separate_props_out => true
assert_match /data-react-class=\"TodoList\"/, html
assert_no_match /{"todos":\["todo1","todo2,","todo3"\]}<\/script>$/, html
props_tag = @helper.render_react_props
assert_not_empty props_tag
assert_match /{"todos":\["todo1","todo2,","todo3"\]}<\/script>$/, props_tag
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 Expand Up @@ -116,15 +134,21 @@ class ViewHelperTest < ActionDispatch::IntegrationTest
end

test 'react server rendering also gets mounted on client' do
visit '/server/1'
assert_match(/data-react-class=\"TodoList\"/, page.html)
assert_match(/data-react-checksum/, page.html)
assert_match(/yep/, page.find("#status").text)
paths = %w(/server/1 /separate/1 /separate/1?move_separate_props_out=true)
paths.each do |path|
visit path
assert_match(/data-react-class=\"TodoList\"/, page.html)
assert_match(/data-react-checksum/, page.html)
assert_match(/yep/, page.find("#status").text)
end
end

test 'react server rendering does not include internal properties' do
visit '/server/1'
assert_no_match(/tag=/, page.html)
assert_no_match(/prerender=/, page.html)
visit '/separate/1'
assert_no_match(/separate_props=/, page.html)
assert_no_match(/move_separate_props_out=/, page.html)
end
end