Skip to content

Conversation

@osjjames
Copy link

@osjjames osjjames commented Apr 25, 2025

What is the purpose of this pull request?

Fixes #56.

What changes did you make? (overview)

#wrapped_in method

Extends the ViewComponentContrib::WrapperComponent to support many dependent child components, such that if none of the registered child components return true from their render? method, the wrapper component will also not render.

This is achieved through a helper method for components called #wrapped_in.

For example, consider here that we only want the content to be rendered if either ExampleA or ExampleB is rendered.

<%= render ViewComponentContrib::WrapperComponent.new do |wrapper| %>
  <div class="flex flex-col gap-4">
    <h3>Title</h3>
    <div class="flex gap-2">
      <%= render ExampleA::Component.new.wrapped_in(wrapper) %>
      <%= render ExampleB::Component.new.wrapped_in(wrapper) %>
    </div>
  </div>
<%- end -%>

It also supports deep nesting of conditional wrappers.

Imagine we have a large section called "Examples", with two sub-sections: "Foo Examples" and "Bar Examples". Each sub-section should only render if it has at least one component rendered inside it, and the large section should only render if at least one sub-section is rendered. We can achieve the behaviour like this:

<!-- Will only render if at least one of the inner wrappers renders -->
<%= render ViewComponentContrib::WrapperComponent.new do |wrapper| %>
  <div class="flex flex-col gap-4">
    <h3>Examples</h3>

    <!-- Will only render if `FooExampleA` or `FooExampleB` renders -->
    <%= render ViewComponentContrib::WrapperComponent.new.wrapped_in(wrapper) do |foo_wrapper| %>
      <div class="flex flex-col gap-4">
        <h4>Foo Examples</h4>
        <div class="flex gap-2">
          <%= render FooExampleA::Component.new.wrapped_in(foo_wrapper) %>
          <%= render FooExampleB::Component.new.wrapped_in(foo_wrapper) %>
        </div>
      </div>
    <%- end -%>

    <!-- Will only render if `BarExampleA` or `BarExampleB` renders -->
    <%= render ViewComponentContrib::WrapperComponent.new.wrapped_in(wrapper) do |bar_wrapper| %>
      <div class="flex flex-col gap-4">
        <h4>Bar Examples</h4>
        <div class="flex gap-2">
          <%= render BarExampleA::Component.new.wrapped_in(bar_wrapper) %>
          <%= render BarExampleB::Component.new.wrapped_in(bar_wrapper) %>
        </div>
      </div>
    <%- end -%>
  </div>
<%- end -%>

#placeholder method

Adds a public method to the ViewComponentContrib::WrapperComponent that allows some placeholder content to be rendered only if none of the wrapper's registered components render.

<%= render ViewComponentContrib::WrapperComponent.new do |wrapper| %>
  <div class="flex flex-col gap-4">
    <h3>Title</h3>
    <div class="flex gap-2">
      <%= render ExampleA::Component.new.wrapped_in(wrapper) %>
      <%= render ExampleB::Component.new.wrapped_in(wrapper) %>
    </div>
  </div>

  <!-- Will only render if neither `ExampleA` nor `ExampleB` render -->
  <%- wrapper.placeholder do -%>
    <span>Examples coming soon!</span>
  <%- end -%>
<%- end -%>

This furthers the goal of keeping conditionals out of the template, and utilises the recursive evaluation of render? methods.

How it works

When #wrapped_in is called on a component, the wrapper component stores it in an array called registered_components. The wrapper's #render? method then simply calls registered_components.any?(&:render?) and uses that result.

A consequence of this is that we lose the benefit of ViewComponent's lazy render evaluation. Child components need to be evaluated in order for #wrapped_in to be called, so a WrapperComponent must have all of its contents evaluated before render.

Is there anything you'd like reviewers to focus on?

  • Naming, particularly of public methods
  • Any simpler way to achieve placeholder functionality that I've missed?
  • Could this integrate any better with the existing behaviour of WrapperComponent? At the moment it has two "modes" that are mutually exclusive

Checklist

  • I've added tests for this change
  • I've added a Changelog entry
  • I've updated a documentation

@palkan
Copy link
Owner

palkan commented May 9, 2025

Hey @osjjames,

Thanks for the proposal.

I like the idea and see how it can be helpful (thanks for the detailed examples). However, the ShowIf terminology/interface looks too verbose; I'd like to iterate and see if we can come up with something better.

Also, I think, we can still re-use the existing WrapperComponent.

Here is what I have in mind:

<%= render ViewComponentContrib::WrapperComponent.new do |wrapper| %>
  <div class="flex flex-col gap-4">
    <h3>Examples</h3>
    <div class="flex gap-2">
      <%= render ExampleA::Component.new.wrapped_in(wrapper) %>
      <%= render ExampleB::Component.new.wrapped_in(wrapper) %>
    </div>
  </div>
<%- end -%>

First, we don't pass any components to the WrapperComponent.new—this way, we indicated the delayed registration of components under consideration.

Then, we introduced the #wrapped_in helper that just tracks the component and invokes their #render? method to see if we need to render anything.

One important difference here (and, I think, this is crucial) is that we don't rely on the HTML content but on the #render? callback, which is in line with the existing WrapperComponent.

WDYT?

@osjjames
Copy link
Author

Hi @palkan, thanks for the review!

Yeah, the verbosity was something that I wasn't happy with either. Your suggested API is a big improvement!

I'll update this PR to reflect that. Should be done within a few days - I'll give you a shout if I'm stuck on anything 👍

@osjjames
Copy link
Author

Hi @palkan, ready for you to take another look 🙌

I've adopted the API you described, as well as adding a #fallback method to the WrapperComponent - it was something I required in my own project, so I've implemented it here. Happy to move that to a separate PR if you prefer.

@osjjames osjjames changed the title Feature: Add ShowIfWrapperComponent to support wrappers conditional on multiple components Feature: Extend WrapperComponent to support it being conditional on multiple components May 16, 2025
@palkan
Copy link
Owner

palkan commented May 16, 2025

Looks great!

I'd only suggest renaming #fallback to #placeholder (use more UI-oriented language), and we're good to merge (well, docs and change logs would be helpful, too).

@osjjames
Copy link
Author

Done 👌 happy to do further docs/changelog updates if needed

@osjjames
Copy link
Author

@palkan bump - anything else needed?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support wrappers with multiple conditional components

2 participants