Skip to content

Commit 82efd0b

Browse files
authored
Add random dom id option (#1121)
* Added configuration option random_dom_id * Added method RenderOptions has_random_dom_id? This new global and react_component helper option allows configuring whether or not React on Rails will automatically add a random id to the DOM node ID.
1 parent 15ba752 commit 82efd0b

File tree

12 files changed

+154
-426
lines changed

12 files changed

+154
-426
lines changed

docs/api/view-helpers-api.md

+15
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
Once the bundled files have been generated in your `app/assets/webpack` folder and you have registered your components, you will want to render these components on your Rails views using the included helper method, `react_component`.
55

6+
------------
7+
68
### react_component
79

810
```ruby
@@ -24,11 +26,13 @@ react_component(component_name,
2426
- **id:** Id for the div, will be used to attach the React component. This will get assigned automatically if you do not provide an id. Must be unique.
2527
- **html_options:** Any other HTML options get placed on the added div for the component. For example, you can set a class (or inline style) on the outer div so that it behaves like a span, with the styling of `display:inline-block`.
2628
- **trace:** set to true to print additional debugging information in the browser. Defaults to true for development, off otherwise. Only on the **client side** will you will see the `railsContext` and your props.
29+
- **random_dom_id:** True to automatically generate random dom ids when using multiple instances of the same React component on one Rails view.
2730
- **options if prerender (server rendering) is true:**
2831
- **replay_console:** Default is true. False will disable echoing server-rendering logs to the browser. While this can make troubleshooting server rendering difficult, so long as you have the configuration of `logging_on_server` set to true, you'll still see the errors on the server.
2932
- **logging_on_server:** Default is true. True will log JS console messages and errors to the server.
3033
- **raise_on_prerender_error:** Default is false. True will throw an error on the server side rendering. Your controller will have to handle the error.
3134

35+
-------------
3236

3337
### react_component_hash
3438

@@ -68,6 +72,8 @@ export default (props, _railsContext) => {
6872

6973
```
7074

75+
------------
76+
7177
### cached_react_component and cached_react_component_hash
7278
Fragment caching is a [React on Rails Pro](https://github.com/shakacode/react_on_rails/wiki) feature. The API is the same as the above, but for 2 differences:
7379

@@ -79,10 +85,13 @@ Fragment caching is a [React on Rails Pro](https://github.com/shakacode/react_on
7985
some_slow_method_that_returns_props
8086
end %>
8187
```
88+
------------
89+
8290
### rails_context
8391
8492
You can call `rails_context(server_side: true | false)` from your controller or view to see what values are are in the Rails Context. Pass true or false depending on whether you want to see the server side or the client side rails_context.
8593
94+
------------
8695
8796
### Renderer Functions (function that will call ReactDOM.render or ReactDOM.hydrate)
8897
@@ -92,6 +101,8 @@ Why would you want to call `ReactDOM.hydrate` yourself? One possible use case is
92101
93102
Renderer functions are not meant to be used on the server since there's no DOM on the server. Instead, use a generator function. Attempting to server render with a renderer function will throw an error.
94103
104+
------------
105+
95106
### React Router
96107
97108
[React Router](https://github.com/reactjs/react-router) is supported, including server-side rendering! See:
@@ -100,6 +111,8 @@ Renderer functions are not meant to be used on the server since there's no DOM o
100111
2. Examples in [spec/dummy/app/views/react_router](../../spec/dummy/app/views/react_router) and follow to the JavaScript code in the [spec/dummy/client/app/startup/ServerRouterApp.jsx](../../spec/dummy/client/app/startup/ServerRouterApp.jsx).
101112
3. [Code Splitting docs](../misc-pending/code-splitting.md) for information about how to set up code splitting for server rendered routes.
102113
114+
------------
115+
103116
## server_render_js
104117
105118
`server_render_js(js_expression, options = {})`
@@ -109,6 +122,8 @@ Renderer functions are not meant to be used on the server since there's no DOM o
109122
110123
This is a helper method that takes any JavaScript expression and returns the output from evaluating it. If you have more than one line that needs to be executed, wrap it in an IIFE. JS exceptions will be caught and console messages handled properly.
111124
125+
------------
126+
112127
# More details
113128
114129
See the [lib/react_on_rails/helper.rb](../../lib/react_on_rails/helper.rb) source.

docs/basics/configuration.md

+9
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,15 @@ ReactOnRails.configure do |config|
1212
# setInterval, clearTimout when server rendering.
1313
config.trace = Rails.env.development?
1414

15+
# Configure if default DOM IDs have a random value or are fixed.
16+
# false ==> Sets the dom id to "#{react_component_name}-react-component"
17+
# true ==> Adds "-#{SecureRandom.uuid}" to that ID
18+
# If you might use multiple instances of the same React component on a Rails page, then
19+
# it is convenient to set this to true or else you have to either manually set the ids to
20+
# avoid collisions. Most newer apps will have only one instance of a component on a page,
21+
# so this should be false in most cases.
22+
# This value can be overrident for a given call to react_component
23+
config.random_dom_id = false # default is true
1524

1625
# defaults to "" (top level)
1726
#

lib/react_on_rails/configuration.rb

+6-3
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ def self.configure
99
DEFAULT_GENERATED_ASSETS_DIR = File.join(%w[public webpack], Rails.env).freeze
1010
DEFAULT_SERVER_RENDER_TIMEOUT = 20
1111
DEFAULT_POOL_SIZE = 1
12+
DEFAULT_RANDOM_DOM_ID = TRUE # for backwards compatability
1213

1314
def self.configuration
1415
@configuration ||= Configuration.new(
@@ -32,7 +33,8 @@ def self.configuration
3233
server_render_method: nil,
3334
symlink_non_digested_assets_regex: nil,
3435
build_test_command: "",
35-
build_production_command: ""
36+
build_production_command: "",
37+
random_dom_id: DEFAULT_RANDOM_DOM_ID
3638
)
3739
end
3840

@@ -45,7 +47,7 @@ class Configuration
4547
:webpack_generated_files, :rendering_extension, :build_test_command,
4648
:build_production_command,
4749
:i18n_dir, :i18n_yml_dir,
48-
:server_render_method, :symlink_non_digested_assets_regex
50+
:server_render_method, :symlink_non_digested_assets_regex, :random_dom_id
4951

5052
def initialize(node_modules_location: nil, server_bundle_js_file: nil, prerender: nil,
5153
replay_console: nil,
@@ -56,7 +58,7 @@ def initialize(node_modules_location: nil, server_bundle_js_file: nil, prerender
5658
generated_assets_dir: nil, webpack_generated_files: nil,
5759
rendering_extension: nil, build_test_command: nil,
5860
build_production_command: nil,
59-
i18n_dir: nil, i18n_yml_dir: nil,
61+
i18n_dir: nil, i18n_yml_dir: nil, random_dom_id: nil,
6062
server_render_method: nil, symlink_non_digested_assets_regex: nil)
6163
self.node_modules_location = node_modules_location.present? ? node_modules_location : Rails.root
6264
self.server_bundle_js_file = server_bundle_js_file
@@ -67,6 +69,7 @@ def initialize(node_modules_location: nil, server_bundle_js_file: nil, prerender
6769
self.i18n_dir = i18n_dir
6870
self.i18n_yml_dir = i18n_yml_dir
6971

72+
self.random_dom_id = random_dom_id
7073
self.prerender = prerender
7174
self.replay_console = replay_console
7275
self.logging_on_server = logging_on_server

lib/react_on_rails/helper.rb

+1
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ def env_stylesheet_link_tag(args = {})
9898
# raise_on_prerender_error: <true/false> Default to false. True will raise exception on server
9999
# if the JS code throws
100100
# Any other options are passed to the content tag, including the id.
101+
# random_dom_id can be set to override the global default.
101102
def react_component(component_name, options = {})
102103
internal_result = internal_react_component(component_name, options)
103104
server_rendered_html = internal_result[:result]["html"]

lib/react_on_rails/react_component/render_options.rb

+24-2
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,26 @@ def props
2222
options.fetch(:props) { NO_PROPS }
2323
end
2424

25+
def random_dom_id
26+
retrieve_key(:random_dom_id)
27+
end
28+
2529
def dom_id
26-
@dom_id ||= options.fetch(:id) { generate_unique_dom_id }
30+
@dom_id ||= options.fetch(:id) do
31+
if random_dom_id
32+
generate_unique_dom_id
33+
else
34+
base_dom_id
35+
end
36+
end
37+
end
38+
39+
def has_random_dom_id?
40+
return false if options[:id]
41+
42+
return false unless random_dom_id
43+
44+
true
2745
end
2846

2947
def html_options
@@ -58,8 +76,12 @@ def to_s
5876

5977
attr_reader :options
6078

79+
def base_dom_id
80+
"#{react_component_name}-react-component"
81+
end
82+
6183
def generate_unique_dom_id
62-
"#{react_component_name}-react-component-#{SecureRandom.uuid}"
84+
"#{base_dom_id}-#{SecureRandom.uuid}"
6385
end
6486

6587
def retrieve_key(key)

spec/dummy/Gemfile.lock

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ GIT
77
PATH
88
remote: ../..
99
specs:
10-
react_on_rails (11.0.9)
10+
react_on_rails (11.0.10)
1111
addressable
1212
connection_pool
1313
execjs (~> 2.5)
@@ -352,4 +352,4 @@ DEPENDENCIES
352352
webpacker
353353

354354
BUNDLED WITH
355-
1.16.2
355+
1.16.3

spec/dummy/config/initializers/react_on_rails.rb

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ def self.custom_context(view_context)
1616
end
1717

1818
ReactOnRails.configure do |config|
19+
config.random_dom_id = false # default is true
1920
config.node_modules_location = "client" # Pre 9.0.0 always used "client"
2021
config.build_production_command = "yarn run build:production"
2122
config.build_test_command = "yarn run build:test"

spec/dummy/spec/helpers/react_on_rails_helper_spec.rb

+63-7
Original file line numberDiff line numberDiff line change
@@ -87,23 +87,34 @@
8787
{ name: "My Test Name" }
8888
end
8989

90-
let(:react_component_div) do
90+
let(:react_component_random_id_div) do
9191
'<div id="App-react-component-0"></div>'
9292
end
9393

94+
let(:react_component_div) do
95+
'<div id="App-react-component"></div>'
96+
end
97+
9498
let(:id) { "App-react-component-0" }
9599

96-
let(:react_definition_script) do
100+
let(:react_definition_script_random) do
97101
<<-SCRIPT.strip_heredoc
98102
<script type="application/json" class="js-react-on-rails-component" \
99103
data-component-name="App" data-dom-id="App-react-component-0">{"name":"My Test Name"}</script>
100104
SCRIPT
101105
end
102106

107+
let(:react_definition_script) do
108+
<<-SCRIPT.strip_heredoc
109+
<script type="application/json" class="js-react-on-rails-component" \
110+
data-component-name="App" data-dom-id="App-react-component">{"name":"My Test Name"}</script>
111+
SCRIPT
112+
end
113+
103114
let(:react_definition_script_no_params) do
104115
<<-SCRIPT.strip_heredoc
105116
<script type="application/json" class="js-react-on-rails-component" \
106-
data-component-name="App" data-dom-id="App-react-component-0">{}</script>
117+
data-component-name="App" data-dom-id="App-react-component">{}</script>
107118
SCRIPT
108119
end
109120

@@ -121,7 +132,7 @@
121132
it { is_expected.to include json_props_sanitized }
122133
end
123134

124-
describe "API with component name only" do
135+
describe "API with component name only (no props or other options)" do
125136
subject { react_component("App") }
126137
it { is_expected.to be_an_instance_of ActiveSupport::SafeBuffer }
127138
it { is_expected.to include react_component_div }
@@ -140,6 +151,51 @@
140151
expect(is_expected.target).to script_tag_be_included(react_definition_script)
141152
}
142153

154+
context "with 'random_dom_id' false option" do
155+
subject { react_component("App", props: props, random_dom_id: false) }
156+
157+
let(:react_definition_script) do
158+
<<-SCRIPT.strip_heredoc
159+
<script type="application/json" class="js-react-on-rails-component" data-component-name="App" data-dom-id="App-react-component">{"name":"My Test Name"}</script>
160+
SCRIPT
161+
end
162+
163+
it { is_expected.to include '<div id="App-react-component"></div>' }
164+
it { expect(is_expected.target).to script_tag_be_included(react_definition_script) }
165+
end
166+
167+
context "with 'random_dom_id' false option" do
168+
subject { react_component("App", props: props, random_dom_id: true) }
169+
170+
let(:react_definition_script) do
171+
<<-SCRIPT.strip_heredoc
172+
<script type="application/json" class="js-react-on-rails-component" data-component-name="App" data-dom-id="App-react-component-0">{"name":"My Test Name"}</script>
173+
SCRIPT
174+
end
175+
176+
it { is_expected.to include '<div id="App-react-component-0"></div>' }
177+
it { expect(is_expected.target).to script_tag_be_included(react_definition_script) }
178+
end
179+
180+
context "with 'random_dom_id' global" do
181+
around(:example) do |example|
182+
ReactOnRails.configure { |config| config.random_dom_id = false }
183+
example.run
184+
ReactOnRails.configure { |config| config.random_dom_id = true }
185+
end
186+
187+
subject { react_component("App", props: props) }
188+
189+
let(:react_definition_script) do
190+
<<-SCRIPT.strip_heredoc
191+
<script type="application/json" class="js-react-on-rails-component" data-component-name="App" data-dom-id="App-react-component">{"name":"My Test Name"}</script>
192+
SCRIPT
193+
end
194+
195+
it { is_expected.to include '<div id="App-react-component"></div>' }
196+
it { expect(is_expected.target).to script_tag_be_included(react_definition_script) }
197+
end
198+
143199
context "with 'id' option" do
144200
subject { react_component("App", props: props, id: id) }
145201

@@ -152,7 +208,7 @@
152208
end
153209

154210
it { is_expected.to include id }
155-
it { is_expected.not_to include react_component_div }
211+
it { is_expected.not_to include react_component_random_id_div }
156212
it {
157213
expect(is_expected.target).to script_tag_be_included(react_definition_script)
158214
}
@@ -217,6 +273,8 @@
217273
ReactOnRails.configuration.rendering_extension = nil
218274
end
219275

276+
after { ReactOnRails.configuration.rendering_extension = @rendering_extension }
277+
220278
it "should not throw an error if not in a view" do
221279
class PlainClass
222280
include ReactOnRailsHelper
@@ -226,7 +284,5 @@ class PlainClass
226284
expect { ob.send(:rails_context, server_side: true) }.to_not raise_error
227285
expect { ob.send(:rails_context, server_side: false) }.to_not raise_error
228286
end
229-
230-
after { ReactOnRails.configuration.rendering_extension = @rendering_extension }
231287
end
232288
end

0 commit comments

Comments
 (0)