diff --git a/docs/getting-started.md b/docs/getting-started.md
index 5617ab5d2..df543d7e9 100644
--- a/docs/getting-started.md
+++ b/docs/getting-started.md
@@ -121,6 +121,24 @@ issue.
};
```
+- You can pass a block to `react_component`, and it will be provided as the `children` prop to the React component, as a React element:
+ ```ruby
+ <%= react_component("YourComponent") do %>
+
Contained HTML
+ <%= render "your/partial" %>
+ <% end %>
+ ```
+ ```js
+ import React from 'react';
+
+ const YourComponent = (props) => (
+
{ props.children }
+ );
+
+ export default YourComponent;
+ ```
+ **Security Note**: This feature is implemented using React's [dangerouslySetInnerHtml](https://react.dev/reference/react-dom/components/common#dangerously-setting-the-inner-html)**. While generally safe when the HTML comes from your own Rails rendering engine, it can expose your application to cross-site scripting (XSS) attacks if not used carefully. Always ensure that the content passed to the block is properly sanitized and comes from trusted sources. Avoid passing user-generated content without proper sanitization.
+
See the [View Helpers API](https://www.shakacode.com/react-on-rails/docs/api/view-helpers-api/) for more details on `react_component` and its sibling function `react_component_hash`.
## Globally Exposing Your React Components
diff --git a/lib/react_on_rails/helper.rb b/lib/react_on_rails/helper.rb
index 97e0953b2..7d883a8b1 100644
--- a/lib/react_on_rails/helper.rb
+++ b/lib/react_on_rails/helper.rb
@@ -53,8 +53,8 @@ module Helper
# Any other options are passed to the content tag, including the id.
# random_dom_id can be set to override the default from the config/initializers. That's only
# used if you have multiple instance of the same component on the Rails view.
- def react_component(component_name, options = {})
- internal_result = internal_react_component(component_name, options)
+ def react_component(component_name, options = {}, &block)
+ internal_result = internal_react_component(component_name, options, &block)
server_rendered_html = internal_result[:result]["html"]
console_script = internal_result[:result]["consoleReplayScript"]
render_options = internal_result[:render_options]
@@ -108,9 +108,10 @@ def react_component(component_name, options = {})
# <% end %>
# <%= react_helmet_app["componentHtml"] %>
#
- def react_component_hash(component_name, options = {})
+ def react_component_hash(component_name, options = {}, &block)
options[:prerender] = true
- internal_result = internal_react_component(component_name, options)
+
+ internal_result = internal_react_component(component_name, options, &block)
server_rendered_html = internal_result[:result]["html"]
console_script = internal_result[:result]["consoleReplayScript"]
render_options = internal_result[:render_options]
@@ -420,7 +421,7 @@ def prepend_render_rails_context(render_value)
"#{rails_context_content}\n#{render_value}".html_safe
end
- def internal_react_component(react_component_name, options = {})
+ def internal_react_component(react_component_name, options = {}, &block)
# Create the JavaScript and HTML to allow either client or server rendering of the
# react_component.
#
@@ -428,6 +429,8 @@ def internal_react_component(react_component_name, options = {})
# (re-hydrate the data). This enables react rendered on the client to see that the
# server has already rendered the HTML.
+ (options[:props] ||= {})[:children_html] = capture(&block) if block
+
render_options = ReactOnRails::ReactComponent::RenderOptions.new(react_component_name: react_component_name,
options: options)
diff --git a/node_package/src/createReactOutput.ts b/node_package/src/createReactOutput.ts
index 1c4572c74..88e192fba 100644
--- a/node_package/src/createReactOutput.ts
+++ b/node_package/src/createReactOutput.ts
@@ -36,13 +36,20 @@ export default function createReactOutput({
console.log(`RENDERED ${name} to dom node with id: ${domNodeId} with props, railsContext:`,
props, railsContext);
}
+
+ if (renderFunction) {
+ console.log(`${name} is a renderFunction`);
+ }
}
+ // Convert any nested content passed to the component into a React element.
+ const children = props?.children_html ? React.createElement('div', {dangerouslySetInnerHTML: {__html: props.children_html }}) : null;
+
+ const createElementFromComponent = (komponent: ReactComponent) => React.createElement(komponent, props, children);
+
if (renderFunction) {
// Let's invoke the function to get the result
- if (trace) {
- console.log(`${name} is a renderFunction`);
- }
+
const renderFunctionResult = (component as RenderFunction)(props, railsContext);
if (isServerRenderHash(renderFunctionResult as CreateReactOutputResult)) {
// We just return at this point, because calling function knows how to handle this case and
@@ -68,8 +75,8 @@ work if you return JSX. Update by wrapping the result JSX of ${name} in a fat ar
// If a component, then wrap in an element
const reactComponent = renderFunctionResult as ReactComponent;
- return React.createElement(reactComponent, props);
+ return createElementFromComponent(reactComponent);
}
// else
- return React.createElement(component as ReactComponent, props);
+ return createElementFromComponent(component as ReactComponent);
}
diff --git a/spec/dummy/app/views/pages/children_example.erb b/spec/dummy/app/views/pages/children_example.erb
new file mode 100644
index 000000000..3ce44ffdf
--- /dev/null
+++ b/spec/dummy/app/views/pages/children_example.erb
@@ -0,0 +1,4 @@
+<%= react_component("ChildrenExample", prerender: true, trace: true) do %>
+ This page demonstrates using Rails to produce content for child nodes of React components
+ And, just to check that multiple DOM nodes are fine
+<% end %>
diff --git a/spec/dummy/app/views/shared/_header.erb b/spec/dummy/app/views/shared/_header.erb
index 020a330f4..0687b745d 100644
--- a/spec/dummy/app/views/shared/_header.erb
+++ b/spec/dummy/app/views/shared/_header.erb
@@ -104,5 +104,8 @@
<%= link_to "Incorrectly wrapping a pure component in a function", pure_component_wrapped_in_function_path %>
+
+ <%= link_to "Children Example", children_example_path %>
+
diff --git a/spec/dummy/client/app/startup/ChildrenExample.jsx b/spec/dummy/client/app/startup/ChildrenExample.jsx
new file mode 100644
index 000000000..f4fc65287
--- /dev/null
+++ b/spec/dummy/client/app/startup/ChildrenExample.jsx
@@ -0,0 +1,10 @@
+import React from 'react';
+
+const ComponentWithChildren = ({ children }) => (
+
+
This is component for testing passing children in from Rails
+ { children }
+
+);
+
+export default ComponentWithChildren;
diff --git a/spec/dummy/config/routes.rb b/spec/dummy/config/routes.rb
index 9dbfbdab4..1f65861e5 100644
--- a/spec/dummy/config/routes.rb
+++ b/spec/dummy/config/routes.rb
@@ -39,6 +39,7 @@
get "react_helmet_broken" => "pages#react_helmet_broken"
get "broken_app" => "pages#broken_app"
get "image_example" => "pages#image_example"
+ get "children_example" => "pages#children_example"
get "context_function_return_jsx" => "pages#context_function_return_jsx"
get "pure_component_wrapped_in_function" => "pages#pure_component_wrapped_in_function"
get "turbo_frame_tag_hello_world" => "pages#turbo_frame_tag_hello_world"
diff --git a/spec/dummy/spec/system/integration_spec.rb b/spec/dummy/spec/system/integration_spec.rb
index deb066c47..d0751d7f9 100644
--- a/spec/dummy/spec/system/integration_spec.rb
+++ b/spec/dummy/spec/system/integration_spec.rb
@@ -284,6 +284,21 @@ def finished_all_ajax_requests?
end
end
+describe "with children", :js do
+ subject { page }
+
+ before { visit "/children_example" }
+
+ it "children_example should not have any errors" do
+ expect(page).to have_text(
+ "This page demonstrates using Rails to produce content for child nodes of React components"
+ )
+ expect(page).to have_text(
+ "And, just to check that multiple DOM nodes are fine"
+ )
+ end
+end
+
describe "use different props for server/client", :js do
subject { page }