Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

referencing slot component predicate methods #2059

Open
mosaaleb opened this issue Jul 19, 2024 · 2 comments
Open

referencing slot component predicate methods #2059

mosaaleb opened this issue Jul 19, 2024 · 2 comments

Comments

@mosaaleb
Copy link

I have a ToolbarComponent that renders a single RansackerComponent, and the RansackerComponent renders multiple FilterComponent slots. Here's a simplified version of the setup:

# toolbar_component.rb
class ToolbarComponent < ViewComponent::Base
  renders_one :ransacker, Ransack::RansackerComponent
end

# ransacker_component.rb
module Ransack
  class RansackerComponent < ViewComponent::Base
    renders_many :filters, ->(field:, select_options:, prompt:, checkbox: false) do
      Ransack::FilterComponent.new(
        field: field,
        ransack_param: param,
        ransack_form_id: id,
        select_options: select_options,
        prompt: prompt,
        checkbox: checkbox,
      )
    end
  end
end

In the view, the components are used like this:

# In views
<%= render ToolbarComponent.new do |toolbar| %>
  <% toolbar.with_ransacker(query: query, url: url) do |ransacker| %>
    <% ransacker.with_filter(field: field, select_options: []) %>
  <% end %>
<% end %>

And the ToolbarComponent template looks like this:

# toolbar_component.html.erb

<%= ransacker if ransacker? %> <!-- order 1 -->

<% if ransacker&.filters? %> <!-- order 2 -->
  <div class="flex gap-4">
    <% ransacker.filters.each do |filter| %>
      <%= filter %>
    <% end %>
  </div>
<% end %>

The ransacker component renders a ransack search form, and the rendering of the filters is delegated to be rendered within the toolbar component.

Problem

If I swap the order of the components (i.e., checking for ransacker.filters? before rendering ransacker), nothing gets rendered at all.

Questions

  • Is my usage of the components valid in this context?
  • Is it safe to call predicates on slots like ransacker&.filters? to check if the slot component has slot components?

Steps to reproduce

Expected behavior

The ToolbarComponent should conditionally render the RansackerComponent and its FilterComponent slots. When the order of checking and rendering components is swapped, the components should still be rendered correctly.

Actual behavior

When the order of checking for ransacker.filters? and rendering ransacker is swapped, nothing gets rendered at all. It seems that the conditional checks on slots like ransacker&.filters? might not be working as expected.

System configuration

Rails version: 7.0.4
Ruby version: 3.1.0
Gem version: 3.0.0

Any insights or recommendations would be greatly appreciated!

@boardfish
Copy link
Collaborator

Hey @mosaaleb, thanks for opening an issue with us! I think this happens because the block passed to with_ransacker isn't called until the ransacker slot is rendered. I don't know if there's a way to figure this out so that we can raise or warn if folks try to access slots in advance of that, but it might be a good thing to add if we can.

This behaviour isn't obvious, but I think it could signal some changes you may want to make to your components.

Looking at this, I would expect that this code indicates the hierarchy of components here:

<%= render ToolbarComponent.new do |toolbar| %>
  <% toolbar.with_ransacker(query: query, url: url) do |ransacker| %>
    <% ransacker.with_filter(field: field, select_options: []) %>
  <% end %>
<% end %>

That is, I'd expect the template for ToolbarComponent to render RansackerComponent, and I'd expect the template for RansackerComponent to be rendering FilterComponents. In this code:

# toolbar_component.html.erb

<%= ransacker if ransacker? %> <!-- order 1 -->

<% if ransacker&.filters? %> <!-- order 2 -->
  <div class="flex gap-4">
    <% ransacker.filters.each do |filter| %>
      <%= filter %>
    <% end %>
  </div>
<% end %>

...ToolbarComponent is reaching down into the RansackerComponent's FilterComponents to render them. So I would first consider the structure of these components – if ToolbarComponent needs to do something based on the filters, perhaps consider passing them into it as data and then using said data in a lambda slot, e.g.:

# ToolbarComponent.new([{ field: field, select_options: []}, {...}])
class ToolbarComponent < ApplicationComponent
  def initialize(filters)
    @filters = filters
  end

  renders_one :ransacker, lambda do |**kwargs|
    RansackerComponent.new(**kwargs)
      .tap do |c|
        @filters.each { |filter_args| c.with_filter(**filter_args) }
      end
    end
  end
end

@reeganviljoen
Copy link
Collaborator

@mosaaleb does the above work for you or is it still an issue

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

No branches or pull requests

3 participants