Skip to content

Content Slots

Phil Schanely edited this page Sep 1, 2021 · 4 revisions

The following situations could warrant a "content slot" approach:

  • a space in a component that could include body copy, particularly the possibility of multiple paragraphs, etc. An example of this could be a body area or caption area for a Media Tile component.
  • a space that a particular child component but may benefit from other possible children. An example of this could be a "Popover" showing beside a heading in that the particular configuration of the popover may need to be quite flexible, or another element might be desired in the spot such as a Label or a Button.
  • an area where a variable set of content may appear. Examples include a button group, toolbar, or grid row.

In these cases it may be preferable to allow for either a property to contain the content or an actual content_for slot. Here are the steps and files affected including patterns that can be used in such cases.

NOTE: The property name we'll use in these examples is body and it is inside a fictitious component called "SageFoo" but this can vary based on context. You can also use these patterns multiple times inside of a given component.

  1. Add an entry for the property and slot in the component definition file sage_foo.rb:

    class SageFoo < SageComponent
      set_attribute_schema(
        body: [:optional, NilClass, String],
        # other properties as needed...
      )
    
      def sections
        %w(foo_body) # more than one slot can be added with a space separating each...
      end
    end
  2. Account for the possible content slot or property in the component view file sage_foo.html.erb:

    <div class="sage-foo">
      <!-- placed in desired position in the view... -->
      <% if component.body.present? or content_for? :sage_foo_body %>
        <div class="sage-foo__body">
          <%= content_for :sage_foo_body if content_for? :sage_foo_body %>
          <%= component.body.html_safe if component.body.present? %>
        </div>
      <% end %>
    </div>

    Note here that this allows for both the property and the slot to be output. Additional logic could be added if you only want one or the other (not both).

  3. (If needed) Build the slot as a property in the React component file Foo.jsx (assuming this is not the primary slot, in which case children should be used):

    export const Foo = ({
      body,
      // other props...
    }) => {
      // component code as needed...
      return (
        <div className="sage-foo">
          {/* placed in desired position in the view... */}
          {body && (
            <div className="sage-foo__body">
              {body}
            </div>
          )} 
        </div>
      );
    };
     
    Foo.defaultProps = {
      body: null,
      // other prop defaults
    };
     
    Foo.propTypes = {
      body: PropTypes.node,
      // other prop types
    };

Now the Rails component can use this feature as follows:

<!-- passing the content to the property directly -->
<%= sage_component SageFoo, {
  body: %(
    <p>Some cool content goes</p>
    <a href="//example.com">Learn more...</a>
  ),
} %>

<!-- using content_for -->
<%= sage_component SageFoo, {} do %>
  <%= content_for :sage_foo_body do %>
    <p>Some cool content goes</p>
    <a href="//example.com">Learn more...</a>
  <% end %>
<% end %>

And in React it can be used as follows:

<Foo
  body={(
    <>
      <p>Some cool content goes</p>
      <a href="//example.com">Learn more...</a>
    </>
  )}
/>