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 }