diff --git a/lib/assets/javascripts/react_ujs.js.erb b/lib/assets/javascripts/react_ujs.js.erb index 7d1262c8d..efe0c4ee6 100644 --- a/lib/assets/javascripts/react_ujs.js.erb +++ b/lib/assets/javascripts/react_ujs.js.erb @@ -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 @@ -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); diff --git a/lib/react/rails/railtie.rb b/lib/react/rails/railtie.rb index 3ea4ae743..c8d00916e 100644 --- a/lib/react/rails/railtie.rb +++ b/lib/react/rails/railtie.rb @@ -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 diff --git a/lib/react/rails/view_helper.rb b/lib/react/rails/view_helper.rb index 5abb55e7a..4d242c598 100644 --- a/lib/react/rails/view_helper.rb +++ b/lib/react/rails/view_helper.rb @@ -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 diff --git a/test/dummy/app/controllers/separate_controller.rb b/test/dummy/app/controllers/separate_controller.rb new file mode 100644 index 000000000..819d5ba64 --- /dev/null +++ b/test/dummy/app/controllers/separate_controller.rb @@ -0,0 +1,2 @@ +class SeparateController < ServerController +end diff --git a/test/dummy/app/views/layouts/application.html.erb b/test/dummy/app/views/layouts/application.html.erb index 670d1875b..4a52a7a76 100644 --- a/test/dummy/app/views/layouts/application.html.erb +++ b/test/dummy/app/views/layouts/application.html.erb @@ -10,5 +10,9 @@ <%= yield %> +
+ +<%= render_react_props %> + diff --git a/test/dummy/app/views/separate/show.html.erb b/test/dummy/app/views/separate/show.html.erb new file mode 100644 index 000000000..597ba8d41 --- /dev/null +++ b/test/dummy/app/views/separate/show.html.erb @@ -0,0 +1 @@ +<%= react_component "TodoList", {:todos => @todos}, :prerender => true, :separate_props => true, :move_separate_props_out => params[:move_separate_props_out] %> diff --git a/test/dummy/config/routes.rb b/test/dummy/config/routes.rb index daf2e2b04..cd8c437fc 100644 --- a/test/dummy/config/routes.rb +++ b/test/dummy/config/routes.rb @@ -1,4 +1,5 @@ Dummy::Application.routes.draw do resources :pages, :only => [:show] resources :server, :only => [:show] + resources :separate, :only => [:show] end diff --git a/test/view_helper_test.rb b/test/view_helper_test.rb index 23c061d43..1604d7cf9 100644 --- a/test/view_helper_test.rb +++ b/test/view_helper_test.rb @@ -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') @@ -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