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

Separate slot setter and getter method #1184

Merged
merged 10 commits into from
May 4, 2022
Merged

Separate slot setter and getter method #1184

merged 10 commits into from
May 4, 2022

Conversation

BlakeWilliams
Copy link
Contributor

@BlakeWilliams BlakeWilliams commented Dec 7, 2021

This introduces an explicit setter for slots in the form of
with_#{slot_name}. This makes it more clear when slots are being set
vs accessed but also allows chaining when slots have no arguments.

Before this change, the following would not work:

component.header.with_content("my header!")

This fails due to header being passed no arguments and no block,
resulting in it acting as a getter, returning nil since the slot was
not defined. This is due to the framework assuming that slots will always
be passed either a block or arguments and predates the with_content
method.

Now, the above can be implemented using with_*:

component.with_header.with_content("my header!")

This introduces an explicit setter for slots in the form of
`with_#{slot_name}`. This makes it more clear when slots are being set
vs accessed but also allows chaining when slots have no arguments.

Before this change, the following would not work:

```ruby
component.header.with_content("my header!")
```

This fails due to `header` being passed no arguments and no block,
resulting in it acting as a getter, returning `nil` since the slot was
not defined. This is due to assuming that slots will always be passed
either a block or arguments and predates the `with_content` method.

Now, the above can be implemented using `with_*`:

```ruby
component.with_header.with_content("my header!")
```
@BlakeWilliams BlakeWilliams marked this pull request as ready for review December 7, 2021 18:32
@BlakeWilliams BlakeWilliams requested a review from a team as a code owner December 7, 2021 18:32
@BlakeWilliams
Copy link
Contributor Author

I didn't add a deprecation notice in this PR, but I think we'll likely decide to deprecate using #{slot_name} as a setter and instead utilize it only as a getter. That seems like a good follow-up for this change since it will give us time to try out the new API and come up with a migration plan.

Co-authored-by: Hans Lemuet <Spone@users.noreply.github.com>
@Spone
Copy link
Collaborator

Spone commented Feb 7, 2022

Hi @BlakeWilliams! I'm wondering if you considered keeping the setter unchanged, but namespacing the getter.

My starting point here is trying to ease the migration of large codebases to the new syntax.

I think the getter is nearly always used internally to the ViewComponent, while the setter is usually called in a lot of places around the app, wherever you render an instance of the ViewComponent with the given slot.

So if only the getter is changed, you would only have to update each ViewComponent using slots (and its template(s)).
Whereas if the setter is changed, it could mean updating hundreds of files.
The former is probably easier to reason about (and maybe even to automate):

  • for each component:
    • open the class
    • if there are renders_one / renders_many
      • find the slots getters in the component class and/or templates and rename them
    • else, do nothing

In terms of syntax, what about something like this:

class MyComponent < ViewComponent::Base
  renders_one :header
end
c = MyComponent.new

# setter
c.header { "Hello world!" }
c.header.with_content("Hello world!")

# getter (suggestions)
c.slots.header # or 
c.header_slot # or 
c.header_content

@BlakeWilliams
Copy link
Contributor Author

@Spone that's a good point. I thought about it from that angle a bit, but I was struggling to come up with an API I was happy with.

Your point about it being easier to migrate is a good one, but I'm not sure we should optimize for that over API ergonomics. Either way, I'm happy to chat about how we could inverse the API like you suggest. I'll comment in the ADR and share some thoughts.

@ViewComponent ViewComponent deleted a comment Feb 8, 2022
@jonspalmer
Copy link
Contributor

@BlakeWilliams I'd agree that the API proposed here has the right ergonomics and we should aim for that vs compromising it for the sake of upgrade ease. For me the trick is going to be to provide the right upgrade path. Perhaps a mechanism to opt in to the old behavior allowing consumers to upgrade legacy components at their leisure.

@ViewComponent ViewComponent deleted a comment Feb 9, 2022
@ViewComponent ViewComponent deleted a comment Feb 10, 2022
docs/CHANGELOG.md Outdated Show resolved Hide resolved
@@ -45,10 +45,16 @@ def register_polymorphic_slot(slot_name, types, collection:)
"#{slot_name}_#{poly_type}"
end

# Deprecated: Will be removed in 3.0
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add a deprecation here that is off by default but can be turned on with a config flag?

@joelhawksley joelhawksley merged commit 1fe5fc9 into main May 4, 2022
@joelhawksley joelhawksley deleted the bmw/slot-setter branch May 4, 2022 21:43
claudiob pushed a commit to claudiob/view_component that referenced this pull request Dec 22, 2023
* Separate slot setter and getter method

This introduces an explicit setter for slots in the form of
`with_#{slot_name}`. This makes it more clear when slots are being set
vs accessed but also allows chaining when slots have no arguments.

Before this change, the following would not work:

```ruby
component.header.with_content("my header!")
```

This fails due to `header` being passed no arguments and no block,
resulting in it acting as a getter, returning `nil` since the slot was
not defined. This is due to assuming that slots will always be passed
either a block or arguments and predates the `with_content` method.

Now, the above can be implemented using `with_*`:

```ruby
component.with_header.with_content("my header!")
```

* Fix linter, add changelog entry

* Update lib/view_component/slotable_v2.rb

Co-authored-by: Hans Lemuet <Spone@users.noreply.github.com>

* Update lib/view_component/slotable_v2.rb

* Update docs/CHANGELOG.md

* Update docs, support polymorphic slots, add tests

* Fix ruby2_keywords call

Co-authored-by: Hans Lemuet <Spone@users.noreply.github.com>
Co-authored-by: Joel Hawksley <joel@hawksley.org>
claudiob pushed a commit to claudiob/view_component that referenced this pull request Jan 3, 2024
* Separate slot setter and getter method

This introduces an explicit setter for slots in the form of
`with_#{slot_name}`. This makes it more clear when slots are being set
vs accessed but also allows chaining when slots have no arguments.

Before this change, the following would not work:

```ruby
component.header.with_content("my header!")
```

This fails due to `header` being passed no arguments and no block,
resulting in it acting as a getter, returning `nil` since the slot was
not defined. This is due to assuming that slots will always be passed
either a block or arguments and predates the `with_content` method.

Now, the above can be implemented using `with_*`:

```ruby
component.with_header.with_content("my header!")
```

* Fix linter, add changelog entry

* Update lib/view_component/slotable_v2.rb

Co-authored-by: Hans Lemuet <Spone@users.noreply.github.com>

* Update lib/view_component/slotable_v2.rb

* Update docs/CHANGELOG.md

* Update docs, support polymorphic slots, add tests

* Fix ruby2_keywords call

Co-authored-by: Hans Lemuet <Spone@users.noreply.github.com>
Co-authored-by: Joel Hawksley <joel@hawksley.org>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants