Skip to content

Commit 8bb7697

Browse files
author
John Lynch
committed
Add view helper that implements server rendering
1 parent 4187c76 commit 8bb7697

File tree

2 files changed

+98
-0
lines changed

2 files changed

+98
-0
lines changed

lib/react/rails.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
require 'react/rails/railtie'
22
require 'react/rails/engine'
3+
require 'react/rails/view_helper'

lib/react/rails/view_helper.rb

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
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

Comments
 (0)