# NOTE: # For any heredoc JS: # 1. The white spacing in this file matters! # 2. Keep all #{some_var} fully to the left so that all indentation is done evenly in that var module ReactOnRailsHelper # react_component_name: can be a React component, created using a ES6 class, or # React.createClass, or a # `generator function` that returns a React component # using ES6 # let MyReactComponentApp = (props) => ; # or using ES5 # var MyReactComponentApp = function(props) { return ; } # Exposing the react_component_name is necessary to both a plain ReactComponent as well as # a generator: # For client rendering, expose the react_component_name on window: # window.MyReactComponentApp = MyReactComponentApp; # For server rendering, export the react_component_name on global: # global.MyReactComponentApp = MyReactComponentApp; # See spec/dummy/client/app/startup/serverGlobals.jsx and # spec/dummy/client/app/startup/ClientApp.jsx for examples of this # props: Ruby Hash which contains the properties to pass to the react object # # options: # generator_function: default is false, set to true if you want to use a # generator function rather than a React Component. # prerender: set to false when debugging! # trace: set to true to print additional debugging information in the browser # default is true for development, off otherwise # replay_console: Default is true. False will disable echoing server rendering # logs to browser. While this can make troubleshooting server rendering difficult, # so long as you have the default configuration of logging_on_server set to # true, you'll still see the errors on the server. # Any other options are passed to the content tag, including the id. def react_component(component_name, props = {}, options = {}) # Create the JavaScript and HTML to allow either client or server rendering of the # react_component. # # Create the JavaScript setup of the global to initialize the client rendering # (re-hydrate the data). This enables react rendered on the client to see that the # server has already rendered the HTML. # We use this react_component_index in case we have the same component multiple times on the page. react_component_index = next_react_component_index react_component_name = component_name.camelize # Not sure if we should be doing this (JG) if options[:id].nil? dom_id = "#{component_name}-react-component-#{react_component_index}" else dom_id = options[:id] end # Setup the page_loaded_js, which is the same regardless of prerendering or not! # The reason is that React is smart about not doing extra work if the server rendering did its job. data_variable_name = "__#{component_name.camelize(:lower)}Data#{react_component_index}__" turbolinks_loaded = Object.const_defined?(:Turbolinks) # NOTE: props might include closing script tag that might cause XSS props_string = sanitized_props_string(props) page_loaded_js = <<-JS (function() { window.#{data_variable_name} = #{props_string}; ReactOnRails.clientRenderReactComponent({ componentName: '#{react_component_name}', domId: '#{dom_id}', propsVarName: '#{data_variable_name}', props: window.#{data_variable_name}, trace: #{trace(options)}, generatorFunction: #{generator_function(options)}, expectTurboLinks: #{turbolinks_loaded} }); })(); JS data_from_server_script_tag = javascript_tag(page_loaded_js) # Create the HTML rendering part server_rendered_html, console_script = server_rendered_react_component_html(options, props_string, react_component_name, data_variable_name, dom_id) content_tag_options = options.except(:generator_function, :prerender, :trace, :replay_console, :id, :react_component_name, :server_side) content_tag_options[:id] = dom_id rendered_output = content_tag(:div, server_rendered_html.html_safe, content_tag_options) # IMPORTANT: Ensure that we mark string as html_safe to avoid escaping. <<-HTML.html_safe #{data_from_server_script_tag} #{rendered_output} #{replay_console(options) ? console_script : ''} HTML end def sanitized_props_string(props) props.is_a?(String) ? json_escape(props) : props.to_json end # Helper method to take javascript expression and returns the output from evaluating it. # If you have more than one line that needs to be executed, wrap it in an IIFE. # JS exceptions are caught and console messages are handled properly. def server_render_js(js_expression, options = {}) wrapper_js = <<-JS (function() { var htmlResult = ''; var consoleReplay = ''; try { htmlResult = (function() { return #{js_expression}; })(); } catch(e) { htmlResult = ReactOnRails.handleError({e: e, componentName: null, jsCode: '#{escape_javascript(js_expression)}', serverSide: true}); } consoleReplay = ReactOnRails.buildConsoleReplay(); return JSON.stringify([htmlResult, consoleReplay]); })() JS result = ReactOnRails::ServerRenderingPool.server_render_js_with_console_logging(wrapper_js) # IMPORTANT: To ensure that Rails doesn't auto-escape HTML tags, use the 'raw' method. raw("#{result[0]}#{replay_console(options) ? result[1] : ''}") end private def next_react_component_index @react_component_index ||= -1 @react_component_index += 1 end # Returns Array [0]: html, [1]: script to console log # NOTE, these are NOT html_safe! def server_rendered_react_component_html(options, props_string, react_component_name, data_variable_name, dom_id) return ["", ""] unless prerender(options) # Make sure that we use up-to-date server-bundle ReactOnRails::ServerRenderingPool.reset_pool_if_server_bundle_was_modified wrapper_js = <<-JS (function() { var props = #{props_string}; return ReactOnRails.serverRenderReactComponent({ componentName: '#{react_component_name}', domId: '#{dom_id}', propsVarName: '#{data_variable_name}', props: props, trace: #{trace(options)}, generatorFunction: #{generator_function(options)} }); })() JS ReactOnRails::ServerRenderingPool.server_render_js_with_console_logging(wrapper_js) rescue ExecJS::ProgramError => err raise ReactOnRails::ServerRenderingPool::PrerenderError.new(react_component_name, props_string, err) end def trace(options) options.fetch(:trace) { ReactOnRails.configuration.trace } end def generator_function(options) options.fetch(:generator_function) { ReactOnRails.configuration.generator_function } end def prerender(options) options.fetch(:prerender) { ReactOnRails.configuration.prerender } end def replay_console(options) options.fetch(:replay_console) { ReactOnRails.configuration.replay_console } end end