Skip to content

Commit 63c7a89

Browse files
Web-Go-ToWeb-Go-To
authored andcommitted
Merge pull request #113 from shakacode/ryanaip-data-tags
Use data attrs when rendering react_components instead of script tags
2 parents 54da213 + d57cea1 commit 63c7a89

File tree

4 files changed

+114
-81
lines changed

4 files changed

+114
-81
lines changed

app/assets/javascripts/react_on_rails.js

Lines changed: 63 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,6 @@
11
(function() {
22
this.ReactOnRails = {};
3-
4-
ReactOnRails.clientRenderReactComponent = function(options) {
5-
var componentName = options.componentName;
6-
var domId = options.domId;
7-
var props = options.props;
8-
var trace = options.trace;
9-
var generatorFunction = options.generatorFunction;
10-
var expectTurboLinks = options.expectTurboLinks;
11-
12-
var renderIfDomNodePresent = function() {
13-
try {
14-
var domNode = document.getElementById(domId);
15-
if (domNode) {
16-
var reactElement = createReactElement(componentName, props,
17-
domId, trace, generatorFunction);
18-
provideClientReact().render(reactElement, domNode);
19-
}
20-
}
21-
catch (e) {
22-
ReactOnRails.handleError({
23-
e: e,
24-
componentName: componentName,
25-
serverSide: false,
26-
});
27-
}
28-
};
29-
30-
var turbolinksInstalled = (typeof Turbolinks !== 'undefined');
31-
if (!expectTurboLinks || (!turbolinksInstalled && expectTurboLinks)) {
32-
if (expectTurboLinks) {
33-
console.warn('WARNING: NO TurboLinks detected in JS, but it is in your Gemfile');
34-
}
35-
36-
document.addEventListener('DOMContentLoaded', function(event) {
37-
renderIfDomNodePresent();
38-
});
39-
} else {
40-
function onPageChange(event) {
41-
var removePageChangeListener = function() {
42-
document.removeEventListener('page:change', onPageChange);
43-
document.removeEventListener('page:before-unload', removePageChangeListener);
44-
var domNode = document.getElementById(domId);
45-
provideClientReact().unmountComponentAtNode(domNode);
46-
};
47-
48-
document.addEventListener('page:before-unload', removePageChangeListener);
49-
50-
renderIfDomNodePresent();
51-
}
52-
53-
document.addEventListener('page:change', onPageChange);
54-
}
55-
};
3+
var turbolinksInstalled = (typeof Turbolinks !== 'undefined');
564

575
ReactOnRails.serverRenderReactComponent = function(options) {
586
var componentName = options.componentName;
@@ -65,7 +13,8 @@
6513
var consoleReplay = '';
6614

6715
try {
68-
var reactElement = createReactElement(componentName, props, domId, trace, generatorFunction);
16+
var reactElement = createReactElement(componentName, props,
17+
domId, trace, generatorFunction);
6918
htmlResult = provideServerReact().renderToString(reactElement);
7019
}
7120
catch (e) {
@@ -151,6 +100,56 @@
151100
return consoleReplay;
152101
};
153102

103+
function forEachComponent(fn) {
104+
var els = document.getElementsByClassName('js-react-on-rails-component');
105+
for (var i = 0; i < els.length; i++) {
106+
fn(els[i]);
107+
};
108+
}
109+
110+
function pageLoaded() {
111+
forEachComponent(render);
112+
}
113+
114+
function pageUnloaded() {
115+
forEachComponent(unmount);
116+
}
117+
118+
function unmount(el) {
119+
var domId = el.getAttribute('data-dom-id');
120+
var domNode = document.getElementById(domId);
121+
provideClientReact().unmountComponentAtNode(domNode);
122+
}
123+
124+
function render(el) {
125+
var componentName = el.getAttribute('data-component-name');
126+
var domId = el.getAttribute('data-dom-id');
127+
var props = JSON.parse(el.getAttribute('data-props'));
128+
var trace = JSON.parse(el.getAttribute('data-trace'));
129+
var generatorFunction = JSON.parse(el.getAttribute('data-generator-function'));
130+
var expectTurboLinks = JSON.parse(el.getAttribute('data-expect-turbo-links'));
131+
132+
if (!turbolinksInstalled && expectTurboLinks) {
133+
console.warn('WARNING: NO TurboLinks detected in JS, but it is in your Gemfile');
134+
}
135+
136+
try {
137+
var domNode = document.getElementById(domId);
138+
if (domNode) {
139+
var reactElement = createReactElement(componentName, props,
140+
domId, trace, generatorFunction);
141+
provideClientReact().render(reactElement, domNode);
142+
}
143+
}
144+
catch (e) {
145+
ReactOnRails.handleError({
146+
e: e,
147+
componentName: componentName,
148+
serverSide: false,
149+
});
150+
}
151+
};
152+
154153
function createReactElement(componentName, props, domId, trace, generatorFunction) {
155154
if (trace) {
156155
console.log('RENDERED ' + componentName + ' to dom node with id: ' + domId);
@@ -178,4 +177,14 @@
178177

179178
return ReactDOMServer;
180179
}
180+
181+
// Install listeners when running on the client.
182+
if (typeof document !== 'undefined') {
183+
if (!turbolinksInstalled) {
184+
document.addEventListener('DOMContentLoaded', pageLoaded);
185+
} else {
186+
document.addEventListener('page:before-unload', pageUnloaded);
187+
document.addEventListener('page:change', pageLoaded);
188+
}
189+
}
181190
}.call(this));

app/helpers/react_on_rails_helper.rb

Lines changed: 25 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ module ReactOnRailsHelper
1919
# global.MyReactComponentApp = MyReactComponentApp;
2020
# See spec/dummy/client/app/startup/serverGlobals.jsx and
2121
# spec/dummy/client/app/startup/ClientApp.jsx for examples of this
22-
# props: Ruby Hash which contains the properties to pass to the react object
22+
# props: Ruby Hash or JSON string which contains the properties to pass to the react object
2323
#
2424
# options:
2525
# generator_function: <true/false> default is false, set to true if you want to use a
@@ -51,27 +51,24 @@ def react_component(component_name, props = {}, options = {})
5151
# Setup the page_loaded_js, which is the same regardless of prerendering or not!
5252
# The reason is that React is smart about not doing extra work if the server rendering did its job.
5353
turbolinks_loaded = Object.const_defined?(:Turbolinks)
54-
# NOTE: props might include closing script tag that might cause XSS
55-
props_string = sanitized_props_string(props)
56-
page_loaded_js = <<-JS
57-
(function() {
58-
var props = #{props_string};
59-
ReactOnRails.clientRenderReactComponent({
60-
componentName: '#{react_component_name}',
61-
domId: '#{dom_id}',
62-
props: props,
63-
trace: #{trace(options)},
64-
generatorFunction: #{generator_function(options)},
65-
expectTurboLinks: #{turbolinks_loaded}
66-
});
67-
})();
68-
JS
6954

70-
data_from_server_script_tag = javascript_tag(page_loaded_js)
55+
component_specification_tag =
56+
content_tag(:div,
57+
"",
58+
class: "js-react-on-rails-component",
59+
style: "display:none",
60+
data: {
61+
component_name: react_component_name,
62+
props: props,
63+
trace: trace(options),
64+
generator_function: generator_function(options),
65+
expect_turbolinks: turbolinks_loaded,
66+
dom_id: dom_id
67+
})
7168

7269
# Create the HTML rendering part
7370
server_rendered_html, console_script =
74-
server_rendered_react_component_html(options, props_string, react_component_name, dom_id)
71+
server_rendered_react_component_html(options, props, react_component_name, dom_id)
7572

7673
content_tag_options = options.except(:generator_function, :prerender, :trace,
7774
:replay_console, :id, :react_component_name,
@@ -84,7 +81,7 @@ def react_component(component_name, props = {}, options = {})
8481

8582
# IMPORTANT: Ensure that we mark string as html_safe to avoid escaping.
8683
<<-HTML.html_safe
87-
#{data_from_server_script_tag}
84+
#{component_specification_tag}
8885
#{rendered_output}
8986
#{replay_console(options) ? console_script : ''}
9087
HTML
@@ -133,12 +130,15 @@ def next_react_component_index
133130

134131
# Returns Array [0]: html, [1]: script to console log
135132
# NOTE, these are NOT html_safe!
136-
def server_rendered_react_component_html(options, props_string, react_component_name, dom_id)
133+
def server_rendered_react_component_html(options, props, react_component_name, dom_id)
137134
return ["", ""] unless prerender(options)
138135

139136
# Make sure that we use up-to-date server-bundle
140137
ReactOnRails::ServerRenderingPool.reset_pool_if_server_bundle_was_modified
141138

139+
# Since this code is not inserted on a web page, we don't need to escape.
140+
props_string = props.is_a?(String) ? props : props.to_json
141+
142142
wrapper_js = <<-JS
143143
(function() {
144144
var props = #{props_string};
@@ -154,7 +154,11 @@ def server_rendered_react_component_html(options, props_string, react_component_
154154

155155
ReactOnRails::ServerRenderingPool.server_render_js_with_console_logging(wrapper_js)
156156
rescue ExecJS::ProgramError => err
157-
raise ReactOnRails::ServerRenderingPool::PrerenderError.new(react_component_name, props_string, err)
157+
raise ReactOnRails::ServerRenderingPool::PrerenderError.new(
158+
react_component_name,
159+
sanitized_props_string(props), # Sanitize as this might be browser logged
160+
err
161+
)
158162
end
159163

160164
def trace(options)

spec/dummy/spec/helpers/react_on_rails_helper_spec.rb

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,26 +35,45 @@
3535
end
3636
end
3737
describe "#react_component" do
38-
subject { react_component("App") }
38+
subject { react_component("App", props) }
39+
40+
let(:props) do
41+
{ name: "My Test Name" }
42+
end
3943

4044
let(:react_component_div) do
4145
"<div id=\"App-react-component-0\"></div>"
4246
end
4347

48+
let(:id) { "App-react-component-0" }
49+
50+
let(:react_definition_div) do
51+
"<div class=\"js-react-on-rails-component\"
52+
style=\"display:none\"
53+
data-component-name=\"App\"
54+
data-props=\"{&quot;name&quot;:&quot;My Test Name&quot;}\"
55+
data-trace=\"false\"
56+
data-generator-function=\"false\"
57+
data-expect-turbolinks=\"true\"
58+
data-dom-id=\"#{id}\"></div>".squish
59+
end
60+
4461
it { expect(self).to respond_to :react_component }
4562

4663
it { is_expected.to be_an_instance_of ActiveSupport::SafeBuffer }
47-
it { is_expected.to start_with "<script>" }
64+
it { is_expected.to start_with "<div" }
4865
it { is_expected.to end_with "</div>\n\n" }
4966
it { is_expected.to include react_component_div }
67+
it { is_expected.to include react_definition_div }
5068

5169
context "with 'id' option" do
52-
subject { react_component("App", {}, id: id) }
70+
subject { react_component("App", props, id: id) }
5371

5472
let(:id) { "shaka_div" }
5573

5674
it { is_expected.to include id }
5775
it { is_expected.not_to include react_component_div }
76+
it { is_expected.to include react_definition_div }
5877
end
5978
end
6079

spec/dummy/spec/requests/console_logging_spec.rb

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
html_nodes = Nokogiri::HTML(response.body)
77

88
expected = <<-JS
9-
console.log.apply(console, ["[SERVER] RENDERED HelloWorldWithLogAndThrow to dom node with id: \
10-
HelloWorldWithLogAndThrow-react-component-0"]);
9+
console.log.apply(console, ["[SERVER] RENDERED HelloWorldWithLogAndThrow to dom node \
10+
with id: HelloWorldWithLogAndThrow-react-component-0"]);
1111
console.log.apply(console, ["[SERVER] console.log in HelloWorld"]);
1212
console.warn.apply(console, ["[SERVER] console.warn in HelloWorld"]);
1313
console.error.apply(console, ["[SERVER] console.error in HelloWorld"]);
@@ -18,7 +18,8 @@
1818

1919
expected_lines = expected.split("\n")
2020

21-
script_node = html_nodes.css("script")[2]
21+
script_node = html_nodes.css("script")[1]
22+
2223
expected_lines.each do |line|
2324
expect(script_node.inner_text).to include(line)
2425
end

0 commit comments

Comments
 (0)