require "react_on_rails/react_renderer" # 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) install_render_events = turbolinks_loaded ? turbolinks_bootstrap(dom_id) : non_turbolinks_bootstrap props_string = props.is_a?(String) ? props : props.to_json page_loaded_js = <<-JS (function() { window.#{data_variable_name} = #{props_string}; #{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, 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, 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 next_react_component_index @react_component_index ||= -1 @react_component_index += 1 end # Returns Array [0]: html, [1]: script to console log def server_rendered_react_component_html(options, props_string, react_component_name, data_variable, dom_id) if prerender(options) render_js_expression = <<-JS (function(React) { #{debug_js(react_component_name, data_variable, dom_id, trace(options))} var reactElement = #{render_js_react_element(react_component_name, props_string, 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 options[:server_side] = true render_js_internal(render_js_expression, options) else ["",""] end 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 = {}) result = render_js_internal(js_expression, options) "#{result[0]}\n#{result[1]}".html_safe end private # 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. # Returns Array [0]: html, [1]: script to console log def render_js_internal(js_expression, options = {}) # TODO: This should be changed so that we don't create a new context every time # Example of doing this here: https://github.com/reactjs/react-rails/tree/master/lib/react/rails ReactOnRails::ReactRenderer.new(options).render_js(js_expression, options) 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 def debug_js(react_component_name, data_variable, dom_id, trace) if trace "console.log(\"RENDERED #{react_component_name} with data_variable"\ " #{data_variable} to dom node with id: #{dom_id}\");" 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