|
| 1 | +module React |
| 2 | + module Rails |
| 3 | + module ViewHelper |
| 4 | + # Render a React component named +name+. Returns the server-rendered HTML |
| 5 | + # as well as javascript to activate the component client-side. |
| 6 | + # The HTML tag is +div+ by default and can be changed by +options[:tag]+. |
| 7 | + # If +options[:tag]+ is a symbol, use it as +options[:tag]+. HTML attributes |
| 8 | + # can be specified by +options+. The javascript will encode +args+ to JSON |
| 9 | + # and use it to construct the component. |
| 10 | + # |
| 11 | + # The server rendering requires you to have a +components.js+ file accessible to |
| 12 | + # Sprockets that contains all of your React components defined along with any code |
| 13 | + # necessary for React to server render them. |
| 14 | + # |
| 15 | + # ==== Examples |
| 16 | + # |
| 17 | + # # // <HelloMessage> defined in a .jsx file: |
| 18 | + # # var HelloMessage = React.createClass({ |
| 19 | + # # render: function() { |
| 20 | + # # return <div>{'Hello ' + this.props.name}</div>; |
| 21 | + # # } |
| 22 | + # # }); |
| 23 | + # react_component(:HelloMessage, :name => 'John') |
| 24 | + # |
| 25 | + # # Use <span> instead of <div>: |
| 26 | + # react_component(:HelloMessage, {:name => 'John'}, :span) |
| 27 | + # react_component(:HelloMessage, {:name => 'John'}, :tag => :span) |
| 28 | + # |
| 29 | + # # Add HTML attributes: |
| 30 | + # react_component(:HelloMessage, {}, {:class => 'c', :id => 'i'}) |
| 31 | + # |
| 32 | + def react_component(name, args = {}, options = {}) |
| 33 | + html_tag, html_options = *react_parse_options(options) |
| 34 | + result = content_tag(html_tag, react_html(name, args), html_options) |
| 35 | + result << react_javascript_tag(name, html_options[:id], args) |
| 36 | + end |
| 37 | + |
| 38 | + private |
| 39 | + # Returns +[html_tag, html_options]+. |
| 40 | + def react_parse_options(options) |
| 41 | + # Syntactic sugar for specifying html tag. |
| 42 | + return [options, {:id => SecureRandom::hex}] if options.is_a?(Symbol) |
| 43 | + |
| 44 | + # Assign a random id if missing. |
| 45 | + options = options.reverse_merge(:id => SecureRandom::hex) |
| 46 | + |
| 47 | + # Use <div> by default. |
| 48 | + tag = options[:tag] || :div |
| 49 | + options.delete(:tag) |
| 50 | + |
| 51 | + [tag, options] |
| 52 | + end |
| 53 | + |
| 54 | + # Keep a module-level copy of the js VM. Note that we are depending on the underlying |
| 55 | + # VM to be threadsafe. |
| 56 | + def react_context |
| 57 | + @@react_context ||= begin |
| 58 | + react_code = File.read(::React::Source.bundled_path_for("react-with-addons.min.js")) |
| 59 | + components_code = ::Rails.application.assets['components.js'].to_s |
| 60 | + all_code = <<-CODE |
| 61 | + var global = global || this; |
| 62 | + #{react_code}; |
| 63 | + React = global.React; |
| 64 | + #{components_code}; |
| 65 | + CODE |
| 66 | + ExecJS.compile(all_code) |
| 67 | + end |
| 68 | + end |
| 69 | + |
| 70 | + def react_html(component, args={}) |
| 71 | + # This works because even though renderComponentToString uses a callback API it is really synchronous |
| 72 | + jscode = <<-JS |
| 73 | + function() { |
| 74 | + var html = ""; |
| 75 | + React.renderComponentToString(#{component}(#{args.to_json}), function(s){html = s}); |
| 76 | + return html; |
| 77 | + }() |
| 78 | + JS |
| 79 | + react_context.eval(jscode).html_safe |
| 80 | + end |
| 81 | + |
| 82 | + def react_javascript_tag(component, mount_node_id, args={}) |
| 83 | + <<-HTML.html_safe |
| 84 | + <script type='text/javascript'> |
| 85 | + React.renderComponent(#{component}(#{args.to_json}), document.getElementById("#{mount_node_id}")) |
| 86 | + </script> |
| 87 | + HTML |
| 88 | + end |
| 89 | + |
| 90 | + end |
| 91 | + end |
| 92 | +end |
| 93 | + |
| 94 | +ActionView::Base.class_eval do |
| 95 | + include ::React::Rails::ViewHelper |
| 96 | +end |
| 97 | + |
0 commit comments