require 'react_on_rails/react_renderer' 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, which can make troubleshooting server rendering difficult. 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) dom_id = "#{component_name}-react-component-#{react_component_index}" # 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) install_render_events = turbolinks_loaded ? turbolinks_bootstrap(dom_id) : non_turbolinks_bootstrap page_loaded_js = <<-JS (function() { window.#{data_variable_name} = #{props.to_json}; #{define_render_if_dom_node_present(react_component_name, data_variable_name, dom_id, trace(options), generator_function(options))} #{install_render_events} })(); JS data_from_server_script_tag = javascript_tag(page_loaded_js) # Create the HTML rendering part server_rendered_html = server_rendered_react_component_html(options, props, react_component_name) rendered_output = content_tag(:div, server_rendered_html, id: dom_id) # IMPORTANT: Ensure that we mark string as html_safe to avoid escaping. <<-HTML.html_safe #{data_from_server_script_tag} #{rendered_output} HTML end def next_react_component_index @react_component_index ||= -1 @react_component_index += 1 end def server_rendered_react_component_html(options, props, react_component_name) if prerender(options) render_js_expression = <<-JS (function(React) { var reactElement = #{render_js_react_element(react_component_name, props.to_json, generator_function(options))}; return React.renderToString(reactElement); })(this.React); JS # create the server generated html of the react component with props options[:react_component_name] = react_component_name server_rendered_react_component_html = render_js(render_js_expression, options) else server_rendered_react_component_html = "" end server_rendered_react_component_html end # Takes javascript code and returns the output from it. This is called by react_component, which # sets up the JS code for rendering a react component. # This method could be used by itself to render the output of any javascript that returns a # string of proper HTML. def render_js(js_expression, options = {}) ReactOnRails::ReactRenderer.new(options).render_js(js_expression, options).html_safe end private 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 debug_js(react_component_name, data_variable, dom_id, trace) if trace <<-JS console.log("CLIENT SIDE RENDERED #{react_component_name} with data_variable #{data_variable} to dom node with id: #{dom_id}"); JS else "" end end # react_component_name: See app/helpers/react_on_rails_helper.rb:5 # props_string: is either the variable name used to hold the props (client side) or the # stringified hash of props from the Ruby server side. In terms of the view helper, one is # simply passing in the Ruby Hash of props. # # Returns the JavaScript code to generate a React element. def render_js_react_element(react_component_name, props_string, generator_function) # "this" is defined by the calling context which is "global" in the execJs # environment or window in the client side context. js_create_element = if generator_function "#{react_component_name}(props)" else "React.createElement(#{react_component_name}, props)" end <<-JS (function(React) { var props = #{props_string}; return #{js_create_element}; })(this.React); JS end def define_render_if_dom_node_present(react_component_name, data_variable, dom_id, trace, generator_function) inner_js_code = <<-JS_CODE var domNode = document.getElementById('#{dom_id}'); if (domNode) { #{debug_js(react_component_name, data_variable, dom_id, trace)} var reactElement = #{render_js_react_element(react_component_name, data_variable, generator_function)}; React.render(reactElement, domNode); } JS_CODE <<-JS var renderIfDomNodePresent = function() { #{ReactOnRails::ReactRenderer.wrap_code_with_exception_handler(inner_js_code, react_component_name)} } JS end def non_turbolinks_bootstrap <<-JS document.addEventListener("DOMContentLoaded", function(event) { console.log("DOMContentLoaded event fired"); renderIfDomNodePresent(); }); JS end def turbolinks_bootstrap(dom_id) <<-JS var turbolinksInstalled = typeof(Turbolinks) !== 'undefined'; if (!turbolinksInstalled) { console.warn("WARNING: NO TurboLinks detected in JS, but it's in your Gemfile"); #{non_turbolinks_bootstrap} } else { function onPageChange(event) { var removePageChangeListener = function() { document.removeEventListener("page:change", onPageChange); document.removeEventListener("page:before-unload", removePageChangeListener); var domNode = document.getElementById('#{dom_id}'); React.unmountComponentAtNode(domNode); }; document.addEventListener("page:before-unload", removePageChangeListener); renderIfDomNodePresent(); } document.addEventListener("page:change", onPageChange); } JS end end