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

Add with_slot_content helper for renders_one slots #1175

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

willcosgrove
Copy link
Contributor

@willcosgrove willcosgrove commented Dec 1, 2021

Summary

This PR adds a new method for every renders_one slot that lets you set the content of the slot directly on the parent instance.

class BlogComponent < ViewComponent::Base
  renders_one :header
  renders_one :subtitle
end
<%= render BlogComponent.new.with_header_content("My blog").with_subtitle_content("Ramblings of a ViewComponent developer") %>

Other Information

This API was suggested by @joelhawksley after looking at a component of mine that was attempting to allow setting the content of two slots both via arguments to the component initializer, and via a block.

class DisabledComponent < ApplicationComponent
  renders_one :enabled
  renders_one :disabled

  def initialize(enabled: nil, disabled: nil)
    self.enabled { enabled } if enabled
    self.disabled { disabled } if disabled
  end
end
<!-- disabled_component.html.erb -->
<span class="group-disabled:hidden contents"><%= enabled %></span>
<span class="hidden group-disabled:contents"><%= disabled %></span>
<%= render DisabledComponent.new(enabled: "Apply", disabled: "Applying...") %>
<!-- Or for a more complex use case -->
<%= render DisabledComponent.new do |c| %>
  <% c.enabled do %>
    Apply 
  <% end %>

  <% c.disabled do %>
    <i class="fa fa-spinner fa-animate-spin" />
    Applying...
  <% end %>
<% end %>

The suggested framework solution for supporting a pattern like this was to add a helper on the component instance that allowed setting the slot content via a method argument. Here, I propose with_#{slot_name}_content as a possible option.

With the changes in this PR, my above component definition would become:

class DisabledComponent < ApplicationComponent
  renders_one :enabled
  renders_one :disabled
end

And the usage of the component could then become:

<%= render DisabledComponent.new.with_enabled_content("Apply").with_disabled_content("Applying...") %>

And the complex use case example would retain the same block based API as before.

@BlakeWilliams
Copy link
Member

@BlakeWilliams BlakeWilliams commented Dec 3, 2021

Thanks for opening this!

I think this approach makes sense, but I think there's a few points worth discussing. I think we'll want to support renders_many slots in addition to the existing support for renders_one slots.

I think there's also one drawback to the API, which is that arguments can't be passed to the slots which narrows the possible use-cases for this functionality.

We've been discussing changing the slots API to have with_#{slot_name} as an explicit setter, so that with_content can be called on Slots that define no arguments.

e.g. with the change to the slots API, the following could work:

<%= render DisabledComponent.new do |c| %>
  <% c.with_enabled.with_content('enabled') %>
  <% c.with_disabled.with_content('disabled') %>
<% end %>

There's some overlap there, so I'm curious if that would help this particular use case.

@willcosgrove
Copy link
Contributor Author

@willcosgrove willcosgrove commented Dec 8, 2021

Thanks for looking at it @BlakeWilliams!

I can see the argument for supporting renders_many for consistency across the two slot types. I can't envision a scenario when I would use a with_#{slot_name}_content for setting a renders_many slot more than once.

BlogComponent.new.with_post_content("My blog post content").with_post_content("My other blog post")

I could see using it if I was only setting one, but the slot happened to be a renders_many for other use cases.

BreadcrumbComponent.new.with_crumb_content("Home")

We've been discussing changing the slots API to have with_#{slot_name} as an explicit setter, so that with_content can be called on Slots that define no arguments.

e.g. with the change to the slots API, the following could work:

<%= render DisabledComponent.new do |c| %>
 <% c.with_enabled.with_content('enabled') %>
 <% c.with_disabled.with_content('disabled') %>
<% end %>

There's some overlap there, so I'm curious if that would help this particular use case.

My main use case was to avoid passing a block to my DisabledComponent to set some string values that could easily just be arguments. I think the cleanest API is probably what I started with DisabledComponent.new(enabled: "Apply", disabled: "Applying...") but I don't think it would generalize well to all slot names, and all components. And, in any case, I don't think it's desired/good/wanted to have ViewComponent start messing with the initializer. It would be very backwards incompatible and would just generally be hard to extend when writing your own initializers.

So while I like specifying the content for the slot as a named argument in my particular case, I don't think it's a good candidate for inclusion in the framework. But I think the with_#{slot_name}_content method is a better general API for achieving the same ends. To my eye, it reads more clearly than with_#{slot_name}.with_content. And because it's defined directly on the component instance, it makes chaining multiple with_#{slot_name}_content more ergonomic.

Those are my 2¢. It won't hurt my feelings though if the decision is that this API isn't one you want the framework to support 😄

@github github deleted a comment from primer-css May 23, 2022
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.

None yet

2 participants