Skip to content

Commit b308749

Browse files
committed
Merge pull request #95 from xionon/jbuilder-integration
Allow react_component to accept JSON strings
2 parents 9d9c41e + 98dbd48 commit b308749

File tree

6 files changed

+104
-4
lines changed

6 files changed

+104
-4
lines changed

README.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,61 @@ react_component('HelloMessage', {name: 'John'}, {id: 'hello', class: 'foo', tag:
111111
# <span class="foo" id="hello" data-...></span>
112112
```
113113

114+
#### With JSON and Jbuilder
115+
116+
You can pass prepared JSON directly to the helper, as well.
117+
118+
```ruby
119+
react_component('HelloMessage', {name: 'John'}.to_json)
120+
# <div data-react-class="HelloMessage" data-react-props="{&quot;name&quot;:&quot;John&quot;}"></div>
121+
```
122+
123+
This is especially helpful if you are already using a tool like Jbuilder in your project.
124+
125+
```ruby
126+
# messages/show.json.jbuilder
127+
json.name name
128+
```
129+
130+
```ruby
131+
react_component('HelloMessage', render(template: 'messages/show.json.jbuilder', locals: {name: 'John'}))
132+
# <div data-react-class="HelloMessage" data-react-props="{&quot;name&quot;:&quot;John&quot;}"></div>
133+
```
134+
135+
##### Important Note
136+
137+
By default, the scaffolded Rails index jbuilder templates do not include a root-node. An example scaffolded index.json.jbuilder looks like this:
138+
139+
```ruby
140+
json.array!(@messages) do |message|
141+
json.extract! message, :id, :name
142+
json.url message_url(message, format: :json)
143+
end
144+
```
145+
146+
which generates JSON like this:
147+
148+
```json
149+
[{"id":1,"name":"hello","url":"http://localhost:3000/messages/1.json"},{"id":2,"name":"hello","url":"http://localhost:3000/messages/2.json"},{"id":3,"name":"hello","url":"http://localhost:3000/messages/3.json"}]
150+
```
151+
152+
This is not suitable for ReactJS props, which is expected to be a key-value object. You will need to wrap your index.json.jbuilder node with a root node, like so:
153+
154+
```ruby
155+
json.messages do |json|
156+
json.array!(@messages) do |message|
157+
json.extract! message, :id, :name
158+
json.url message_url(message, format: :json)
159+
end
160+
end
161+
```
162+
163+
Which will generate:
164+
165+
```json
166+
{"messages":[{"id":1,"name":"hello","url":"http://localhost:3000/messages/1.json"},{"id":2,"name":"hello","url":"http://localhost:3000/messages/2.json"},{"id":3,"name":"hello","url":"http://localhost:3000/messages/3.json"}]}
167+
```
168+
114169
### Server Rendering
115170

116171
React components can also use the same ExecJS mechanisms in Sprockets to execute JavaScript code on the server, and render React components to HTML to be delivered to the browser, and then the `react_ujs` script will cause the component to be mounted. In this way, users get fast initial page loads and search-engine-friendly pages.

lib/react/rails/view_helper.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ def react_component(name, args = {}, options = {}, &block)
1313
html_options = options.reverse_merge(:data => {})
1414
html_options[:data].tap do |data|
1515
data[:react_class] = name
16-
data[:react_props] = args.to_json unless args.empty?
16+
data[:react_props] = React::Renderer.react_props(args) unless args.empty?
1717
end
1818
html_tag = html_options[:tag] || :div
1919

lib/react/renderer.rb

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ class Renderer
55

66
class PrerenderError < RuntimeError
77
def initialize(component_name, props, js_message)
8-
message = "Encountered error \"#{js_message}\" when prerendering #{component_name} with #{props.to_json}"
8+
message = "Encountered error \"#{js_message}\" when prerendering #{component_name} with #{props}"
99
super(message)
1010
end
1111
end
@@ -54,19 +54,28 @@ def self.combined_js
5454
@@combined_js
5555
end
5656

57+
def self.react_props(args={})
58+
if args.is_a? String
59+
args
60+
else
61+
args.to_json
62+
end
63+
end
64+
5765
def context
5866
@context ||= ExecJS.compile(self.class.combined_js)
5967
end
6068

6169
def render(component, args={})
70+
react_props = React::Renderer.react_props(args)
6271
jscode = <<-JS
6372
function() {
64-
return React.renderComponentToString(#{component}(#{args.to_json}));
73+
return React.renderComponentToString(#{component}(#{react_props}));
6574
}()
6675
JS
6776
context.eval(jscode).html_safe
6877
rescue ExecJS::ProgramError => e
69-
raise PrerenderError.new(component, args, e)
78+
raise PrerenderError.new(component, react_props, e)
7079
end
7180
end
7281
end

react-rails.gemspec

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ Gem::Specification.new do |s|
2121
s.add_development_dependency 'es5-shim-rails', '>= 2.0.5'
2222
s.add_development_dependency 'poltergeist', '>= 0.3.3'
2323

24+
s.add_development_dependency 'jbuilder'
25+
2426
s.add_dependency 'execjs'
2527
s.add_dependency 'rails', '>= 3.1'
2628
s.add_dependency 'react-source', '0.11.1'

test/react_renderer_test.rb

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,16 @@ class ReactRendererTest < ActiveSupport::TestCase
88
assert_match /data-react-checksum/, result
99
end
1010

11+
test 'Server rendering with an already-encoded json string' do
12+
json_string = Jbuilder.new do |json|
13+
json.todos %w{todo1 todo2 todo3}
14+
end.target!
15+
16+
result = React::Renderer.render "TodoList", json_string
17+
assert_match /todo1.*todo2.*todo3/, result
18+
assert_match /data-react-checksum/, result
19+
end
20+
1121
test 'Rendering does not throw an exception when console log api is used' do
1222
%W(error info log warn).each do |fn|
1323
assert_nothing_raised(ExecJS::ProgramError) do
@@ -23,4 +33,17 @@ class ReactRendererTest < ActiveSupport::TestCase
2333
expected_message = 'Encountered error "ReferenceError: NonexistentComponent is not defined" when prerendering NonexistentComponent with {"error":true,"exists":false}'
2434
assert_equal expected_message, err.message
2535
end
36+
37+
test 'prerender errors are thrown when given a string' do
38+
json_string = Jbuilder.new do |json|
39+
json.error true
40+
json.exists false
41+
end.target!
42+
43+
err = assert_raises React::Renderer::PrerenderError do
44+
React::Renderer.render("NonexistentComponent", json_string)
45+
end
46+
expected_message = 'Encountered error "ReferenceError: NonexistentComponent is not defined" when prerendering NonexistentComponent with {"error":true,"exists":false}'
47+
assert_equal expected_message, err.message
48+
end
2649
end

test/view_helper_test.rb

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,17 @@ class ViewHelperTest < ActionDispatch::IntegrationTest
2929
end
3030
end
3131

32+
test 'react_component accepts jbuilder-based strings as properties' do
33+
jbuilder_json = Jbuilder.new do |json|
34+
json.bar 'value'
35+
end.target!
36+
37+
html = @helper.react_component('Foo', jbuilder_json)
38+
%w(data-react-class="Foo" data-react-props="{&quot;bar&quot;:&quot;value&quot;}").each do |segment|
39+
assert html.include?(segment), "expected #{html} to include #{segment}"
40+
end
41+
end
42+
3243
test 'react_component accepts HTML options and HTML tag' do
3344
assert @helper.react_component('Foo', {}, :span).match(/<span\s.*><\/span>/)
3445

0 commit comments

Comments
 (0)