diff --git a/lib/react/server_rendering.rb b/lib/react/server_rendering.rb index c379be1a..e6774cab 100644 --- a/lib/react/server_rendering.rb +++ b/lib/react/server_rendering.rb @@ -1,4 +1,5 @@ require 'connection_pool' +require 'react/server_rendering/exec_js_renderer' require 'react/server_rendering/sprockets_renderer' module React @@ -20,5 +21,13 @@ def self.render(component_name, props, prerender_options) def self.create_renderer renderer.new(renderer_options) end + + class PrerenderError < RuntimeError + def initialize(component_name, props, js_message) + message = ["Encountered error \"#{js_message}\" when prerendering #{component_name} with #{props}", + js_message.backtrace.join("\n")].join("\n") + super(message) + end + end end end \ No newline at end of file diff --git a/lib/react/server_rendering/exec_js_renderer.rb b/lib/react/server_rendering/exec_js_renderer.rb new file mode 100644 index 00000000..52ebf2a0 --- /dev/null +++ b/lib/react/server_rendering/exec_js_renderer.rb @@ -0,0 +1,40 @@ +# A bare-bones renderer for React.js + Exec.js +# - No Rails dependency +# - No browser concerns +module React + module ServerRendering + class ExecJSRenderer + def initialize(options={}) + js_code = options[:code] || raise("Pass `code:` option to instantiate a JS context!") + @context = ExecJS.compile(GLOBAL_WRAPPER + js_code) + end + + def render(component_name, props, prerender_options) + render_function = prerender_options.fetch(:render_function, "renderToString") + js_code = <<-JS + (function () { + #{before_render(component_name, props, prerender_options)} + var result = React.#{render_function}(React.createElement(#{component_name}, #{props})); + #{after_render(component_name, props, prerender_options)} + return result; + })() + JS + @context.eval(js_code).html_safe + rescue ExecJS::ProgramError => err + raise React::ServerRendering::PrerenderError.new(component_name, props, err) + end + + # Hooks for inserting JS before/after rendering + def before_render(component_name, props, prerender_options); ""; end + def after_render(component_name, props, prerender_options); ""; end + + # Handle Node.js & other ExecJS contexts + GLOBAL_WRAPPER = <<-JS + var global = global || this; + var self = self || this; + var window = window || this; + JS + + end + end +end diff --git a/lib/react/server_rendering/sprockets_renderer.rb b/lib/react/server_rendering/sprockets_renderer.rb index 716e2355..8813d1d6 100644 --- a/lib/react/server_rendering/sprockets_renderer.rb +++ b/lib/react/server_rendering/sprockets_renderer.rb @@ -1,17 +1,20 @@ +# Extends ExecJSRenderer for the Rails environment +# - builds JS code out of the asset pipeline +# - stringifies props +# - implements console replay module React module ServerRendering - class SprocketsRenderer + class SprocketsRenderer < ExecJSRenderer def initialize(options={}) @replay_console = options.fetch(:replay_console, true) - filenames = options.fetch(:files, ["react.js", "components.js"]) - js_code = GLOBAL_WRAPPER + CONSOLE_POLYFILL + js_code = CONSOLE_POLYFILL.dup filenames.each do |filename| js_code << ::Rails.application.assets[filename].to_s end - @context = ExecJS.compile(js_code) + super(options.merge(code: js_code)) end def render(component_name, props, prerender_options) @@ -26,25 +29,12 @@ def render(component_name, props, prerender_options) props = props.to_json end - js_code = <<-JS - (function () { - var result = React.#{react_render_method}(React.createElement(#{component_name}, #{props})); - #{@replay_console ? CONSOLE_REPLAY : ""} - return result; - })() - JS - - @context.eval(js_code).html_safe - rescue ExecJS::ProgramError => err - raise PrerenderError.new(component_name, props, err) + super(component_name, props, {render_function: react_render_method}) end - # Handle node.js & other RubyRacer contexts - GLOBAL_WRAPPER = <<-JS - var global = global || this; - var self = self || this; - var window = window || this; - JS + def after_render(component_name, props, prerender_options) + @replay_console ? CONSOLE_REPLAY : "" + end # Reimplement console methods for replaying on the client CONSOLE_POLYFILL = <<-JS @@ -68,14 +58,6 @@ def render(component_name, props, prerender_options) } })(console.history); JS - - class PrerenderError < RuntimeError - def initialize(component_name, props, js_message) - message = ["Encountered error \"#{js_message}\" when prerendering #{component_name} with #{props}", - js_message.backtrace.join("\n")].join("\n") - super(message) - end - end end end end diff --git a/test/react/server_rendering/exec_js_renderer_test.rb b/test/react/server_rendering/exec_js_renderer_test.rb new file mode 100644 index 00000000..65474140 --- /dev/null +++ b/test/react/server_rendering/exec_js_renderer_test.rb @@ -0,0 +1,72 @@ +require 'test_helper' + +DUMMY_IMPLEMENTATION = " +var Todo = null +var React = { + renderToString: function() { + return 'renderToString was called' + }, + createElement: function() {} +} +" + +class ExecJSRendererTest < ActiveSupport::TestCase + setup do + react_source = Rails.application.assets["react.js"].to_s + todo_component_source = Rails.application.assets["components/Todo.js"].to_s + @renderer = React::ServerRendering::ExecJSRenderer.new(code: react_source + todo_component_source) + end + + test '#render returns HTML' do + result = @renderer.render("Todo", {todo: "write tests"}.to_json, {}) + assert_match(/