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

.with_collection supports Components with splatted KeywordArgs #1560

Conversation

zspencer
Copy link
Contributor

What are you trying to accomplish?

I'd like to be lazy and not define initialize on every view component when they inherit from a parent class that accepts keyword arguments in order for the component to work with `with_collection.

For example:

class ApplicationComponent < ViewComponent::Base
  def initialize(**kwargs)
    super
    kwargs.each do |k, v|
      send(:"#{k}=", v)
    end
  end
end

class ProductComponent < ApplicationComponent
  attr_accessor :product
end

class OrderComponent < ApplicationComponent
  attr_accessor :order
end

However, ViewComponent doesn't recognize that a keyword args is an implicit def initialize(product:) or def initialize(order:); which causes it to raise an ArgumentError for an unaccepted parameter.

What approach did you choose and why?

I made it so Components with Keyword Args in their initialize pass the initialze parameter name check. This seemed like a small enough change?

Anything you want to highlight for special attention from reviewers?=

  • Are there side-effects of this I'm not aware of?
  • Does this create subtle design pressure that encourages cruft?

In a few projects, I wind up using keyword args on a Base class so that
I don't have to define an `initialize` on every child class. For
example:

```ruby
class ApplicationComponent < ViewComponent::Base
  def initialize(**kwargs)
    super
    kwargs.each do |k, v|
      send(:"#{k}=", v)
    end
  end
end

class ProductComponent < ApplicationComponent
  attr_accessor :product
end

class OrderComponent < ApplicationComponent
  attr_accessor :order
end

```

However, ViewComponent doesn't recognize that a keyword args is an
implicit `def initialize(product:)` or `def initialize(order:)`; which
causes it to raise an `ArgumentError` for an unaccepted parameter.

This causes Components with Keyword Args in their initialize to pass the
initialze parameter name check.

Open Questions:

- [ ] Are there side-effects of this I'm not aware of?
- [ ] Does this create subtle design pressure that encourages cruft?

Thoughts?
@zspencer zspencer marked this pull request as draft October 24, 2022 19:04
@zspencer
Copy link
Contributor Author

Woops, I must have forgotten to include the empty call method definition when I pulled out the class. One moment...

@zspencer zspencer changed the title Initial support for Components with KeywordArgs .with_collection supports Components whose initialize method takes KeywordArgs Oct 24, 2022
@zspencer zspencer force-pushed the enhancement/support-keyword-args-in-components branch from a8b54b3 to 47fd21a Compare October 24, 2022 19:12
@zspencer zspencer marked this pull request as ready for review October 24, 2022 19:14
docs/CHANGELOG.md Outdated Show resolved Hide resolved
Comment on lines 31 to 32
collection = ComponentWithKeywordArgs.with_collection(@products)
render_inline(collection)
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
collection = ComponentWithKeywordArgs.with_collection(@products)
render_inline(collection)
render_inline(ComponentWithKeywordArgs.with_collection(@products))

Can you add an assertion that something is rendered correctly?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done!

@zspencer zspencer force-pushed the enhancement/support-keyword-args-in-components branch from 49034fe to 33a4daf Compare November 7, 2022 23:56
@joelhawksley
Copy link
Member

👋🏻 it looks like you do have a legit failure in CI ❤️

@zspencer
Copy link
Contributor Author

zspencer commented Nov 8, 2022

Woops! I ran them locally but must have misread the output. That's what I get for shoveling all my open-source stuff to end-of-business!

@zspencer zspencer marked this pull request as draft November 8, 2022 16:41
I appear to have forgotten that the with_collection leverages the
components name to figure out the name of the argument it uses for the
collection; rather than the name of the items in the collection (which
is how it works in Rails).
@zspencer zspencer marked this pull request as ready for review November 9, 2022 17:13
@zspencer
Copy link
Contributor Author

zspencer commented Nov 9, 2022

yes-chef

Which is to say, I think the dish is ready? I don't understand the PVC error (I am assuming it's a permissions thing?)

@@ -10,6 +10,10 @@ nav_order: 5

## main

* `Component.with_collection` supports components that accept keyword arguments.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can this be changed to "accept splatted keyword arguments"? I believe defined keyword args are not a problem.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's much clearer!

@@ -591,7 +591,7 @@ def validate_collection_parameter!(validate_default: false)
parameter = validate_default ? collection_parameter : provided_collection_parameter

return unless parameter
return if initialize_parameter_names.include?(parameter)
return if initialize_parameter_names.include?(parameter) || initialize_parameter_names.include?(:kwargs)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Does this imply that the splatted argument has to have the name kwargs? That is, would this work with def initialize(**other_args)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

An excellent question to which I do not yet have a conclusive answer. I'll update the tests and report back.
i have no idea what im doing

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Excellent catch! I've updated to check for :kwrest in the flattened initialize parameters!

Copy link
Collaborator

Choose a reason for hiding this comment

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

Thanks for making that change.

@zspencer zspencer marked this pull request as draft November 11, 2022 19:07
zspencer added a commit to zinc-collective/view_component that referenced this pull request Nov 11, 2022
While working on [supporting splatted keyword
args](ViewComponent#1560) I
discovered that the linter was failing me on changes I didn't make.

When I ran standardrb locally, it was quite content, so I was a bit
confused. Then I read [.github/workflows/lint.yml] and realized that
view_component runs a `bundle update` prior to running standard; which
bumps the standardrb version and the corresponding rubocop set.

This wasintroduced by @camertron in
[e7eec98](ViewComponent@e7eec98)
7+ months ago to fix an error in CI.

It's unclear to me why `bundle install` was not working, so I decided
that rather than swapping that back I would trust @camertron's instincts
and roll-forward with the update.
zspencer added a commit to zinc-collective/view_component that referenced this pull request Nov 11, 2022
While working on [supporting splatted keyword
args](ViewComponent#1560) I
discovered that the linter was failing me on changes I didn't make.

When I ran standardrb locally, it was quite content, so I was a bit
confused. Then I read [.github/workflows/lint.yml] and realized that
view_component runs a `bundle update` prior to running standard; which
bumps the standardrb version and the corresponding rubocop set.

This wasintroduced by @camertron in
[e7eec98](ViewComponent@e7eec98)
7+ months ago to fix an error in CI.

It's unclear to me why `bundle install` was not working, so I decided
that rather than swapping that back I would trust @camertron's instincts
and roll-forward with the update.

I am not including a change to the Changelog because this seems internal
and unnecessary to communicate to package consumers.
@zspencer zspencer marked this pull request as ready for review November 11, 2022 19:34
@zspencer
Copy link
Contributor Author

Once more into the fray! The lint failures appear to be unrelated to my changes; so I made a patch to resolve those as well: #1576

@zspencer zspencer changed the title .with_collection supports Components whose initialize method takes KeywordArgs .with_collection supports Components whose initialize method takes splatted KeywordArgs Nov 11, 2022
@zspencer zspencer changed the title .with_collection supports Components whose initialize method takes splatted KeywordArgs .with_collection supports Components with splatted KeywordArgs Nov 11, 2022
@zspencer
Copy link
Contributor Author

OK look slike the 3.1 build is not happy will poke at it 🔜

@zspencer zspencer marked this pull request as draft November 11, 2022 22:05
joelhawksley pushed a commit that referenced this pull request Nov 14, 2022
While working on [supporting splatted keyword
args](#1560) I
discovered that the linter was failing me on changes I didn't make.

When I ran standardrb locally, it was quite content, so I was a bit
confused. Then I read [.github/workflows/lint.yml] and realized that
view_component runs a `bundle update` prior to running standard; which
bumps the standardrb version and the corresponding rubocop set.

This wasintroduced by @camertron in
[e7eec98](e7eec98)
7+ months ago to fix an error in CI.

It's unclear to me why `bundle install` was not working, so I decided
that rather than swapping that back I would trust @camertron's instincts
and roll-forward with the update.

I am not including a change to the Changelog because this seems internal
and unnecessary to communicate to package consumers.
In Ruby 3.1, it appears that the `ViewComponent::Base#initialize` method
has an implicit key-rest argument, named `:**`.

I thought it may be because we define it with a `def initialize(*); end;`
which could be implicitly adding in the rest of the splattable
parameter? However, playing around with that syntax doesn't seem to be
the cause:

```ruby
class A; def initialize(*); end; end
class B; end
A.instance_method(:initialize).parameters
B.instance_method(:initialize).parameters
```
@zspencer zspencer marked this pull request as ready for review November 14, 2022 22:52
@zspencer
Copy link
Contributor Author

OK, looks like we are back to green on CI; and I have resolved the test failure for ruby 3.1.

@joelhawksley
Copy link
Member

@zspencer can you fix the merge conflicts? ❤️

@zspencer
Copy link
Contributor Author

@joelhawksley - I have several times at this point... They'll keep coming up when y'all merge other stuff, so for me to do it feels like a dog chasing its tail a bit.

@joelhawksley joelhawksley enabled auto-merge (squash) November 16, 2022 19:13
@zspencer
Copy link
Contributor Author

@joelhawksley - You'll need to approve it in order for auto-merge to work, I think.

@joelhawksley joelhawksley merged commit 56a6560 into ViewComponent:main Nov 16, 2022
@joelhawksley
Copy link
Member

That'll do it! Thanks @zspencer ❤️

@zspencer zspencer deleted the enhancement/support-keyword-args-in-components branch November 17, 2022 16:01
@zspencer
Copy link
Contributor Author

autobots-roll-out

claudiob pushed a commit to claudiob/view_component that referenced this pull request Dec 22, 2023
While working on [supporting splatted keyword
args](ViewComponent#1560) I
discovered that the linter was failing me on changes I didn't make.

When I ran standardrb locally, it was quite content, so I was a bit
confused. Then I read [.github/workflows/lint.yml] and realized that
view_component runs a `bundle update` prior to running standard; which
bumps the standardrb version and the corresponding rubocop set.

This wasintroduced by @camertron in
[e7eec98](ViewComponent@e7eec98)
7+ months ago to fix an error in CI.

It's unclear to me why `bundle install` was not working, so I decided
that rather than swapping that back I would trust @camertron's instincts
and roll-forward with the update.

I am not including a change to the Changelog because this seems internal
and unnecessary to communicate to package consumers.
claudiob pushed a commit to claudiob/view_component that referenced this pull request Dec 22, 2023
…wComponent#1560)

* Initial support for Components with KeywordArgs

In a few projects, I wind up using keyword args on a Base class so that
I don't have to define an `initialize` on every child class. For
example:

```ruby
class ApplicationComponent < ViewComponent::Base
  def initialize(**kwargs)
    super
    kwargs.each do |k, v|
      send(:"#{k}=", v)
    end
  end
end

class ProductComponent < ApplicationComponent
  attr_accessor :product
end

class OrderComponent < ApplicationComponent
  attr_accessor :order
end

```

However, ViewComponent doesn't recognize that a keyword args is an
implicit `def initialize(product:)` or `def initialize(order:)`; which
causes it to raise an `ArgumentError` for an unaccepted parameter.

This causes Components with Keyword Args in their initialize to pass the
initialze parameter name check.

Open Questions:

- [ ] Are there side-effects of this I'm not aware of?
- [ ] Does this create subtle design pressure that encourages cruft?

Thoughts?

* Render nothing just to get past the explode boom

* Lint lint lint

* Standardrb vs Ruby 2.5: FIGHT!

* Add Changelog per CI

* Update docs/CHANGELOG.md

Co-authored-by: Joel Hawksley <[email protected]>

* Assert rendering against the collection works

* The Kwarg is the Component name 🙈

I appear to have forgotten that the with_collection leverages the
components name to figure out the name of the argument it uses for the
collection; rather than the name of the items in the collection (which
is how it works in Rails).

* Clarify that this is for Splatted Keyword Args

Co-authored-by: [email protected]

* Keyword argument detection is more resilient

* Don't count un-named splatted keyword args

In Ruby 3.1, it appears that the `ViewComponent::Base#initialize` method
has an implicit key-rest argument, named `:**`.

I thought it may be because we define it with a `def initialize(*); end;`
which could be implicitly adding in the rest of the splattable
parameter? However, playing around with that syntax doesn't seem to be
the cause:

```ruby
class A; def initialize(*); end; end
class B; end
A.instance_method(:initialize).parameters
B.instance_method(:initialize).parameters
```

* Make it actually private

Co-authored-by: Joel Hawksley <[email protected]>
claudiob pushed a commit to claudiob/view_component that referenced this pull request Jan 3, 2024
While working on [supporting splatted keyword
args](ViewComponent#1560) I
discovered that the linter was failing me on changes I didn't make.

When I ran standardrb locally, it was quite content, so I was a bit
confused. Then I read [.github/workflows/lint.yml] and realized that
view_component runs a `bundle update` prior to running standard; which
bumps the standardrb version and the corresponding rubocop set.

This wasintroduced by @camertron in
[e7eec98](ViewComponent@e7eec98)
7+ months ago to fix an error in CI.

It's unclear to me why `bundle install` was not working, so I decided
that rather than swapping that back I would trust @camertron's instincts
and roll-forward with the update.

I am not including a change to the Changelog because this seems internal
and unnecessary to communicate to package consumers.
claudiob pushed a commit to claudiob/view_component that referenced this pull request Jan 3, 2024
…wComponent#1560)

* Initial support for Components with KeywordArgs

In a few projects, I wind up using keyword args on a Base class so that
I don't have to define an `initialize` on every child class. For
example:

```ruby
class ApplicationComponent < ViewComponent::Base
  def initialize(**kwargs)
    super
    kwargs.each do |k, v|
      send(:"#{k}=", v)
    end
  end
end

class ProductComponent < ApplicationComponent
  attr_accessor :product
end

class OrderComponent < ApplicationComponent
  attr_accessor :order
end

```

However, ViewComponent doesn't recognize that a keyword args is an
implicit `def initialize(product:)` or `def initialize(order:)`; which
causes it to raise an `ArgumentError` for an unaccepted parameter.

This causes Components with Keyword Args in their initialize to pass the
initialze parameter name check.

Open Questions:

- [ ] Are there side-effects of this I'm not aware of?
- [ ] Does this create subtle design pressure that encourages cruft?

Thoughts?

* Render nothing just to get past the explode boom

* Lint lint lint

* Standardrb vs Ruby 2.5: FIGHT!

* Add Changelog per CI

* Update docs/CHANGELOG.md

Co-authored-by: Joel Hawksley <[email protected]>

* Assert rendering against the collection works

* The Kwarg is the Component name 🙈

I appear to have forgotten that the with_collection leverages the
components name to figure out the name of the argument it uses for the
collection; rather than the name of the items in the collection (which
is how it works in Rails).

* Clarify that this is for Splatted Keyword Args

Co-authored-by: [email protected]

* Keyword argument detection is more resilient

* Don't count un-named splatted keyword args

In Ruby 3.1, it appears that the `ViewComponent::Base#initialize` method
has an implicit key-rest argument, named `:**`.

I thought it may be because we define it with a `def initialize(*); end;`
which could be implicitly adding in the rest of the splattable
parameter? However, playing around with that syntax doesn't seem to be
the cause:

```ruby
class A; def initialize(*); end; end
class B; end
A.instance_method(:initialize).parameters
B.instance_method(:initialize).parameters
```

* Make it actually private

Co-authored-by: Joel Hawksley <[email protected]>
@@ -636,6 +636,11 @@ def iteration_argument_present?

private

def splatted_keyword_argument_present?
initialize_parameters.flatten.include?(:keyrest) &&
!initialize_parameters.include?([:keyrest, :**]) # Un-named splatted keyword args don't count!

Choose a reason for hiding this comment

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

Is there a specific reason for not accepting unnamed splatted arguments? This limitation creates some problems with initializers, which forward their arguments to helpers, e.g. dry-initialize, which uses def initialize(...); __dry_initializer_initialize__(...); end (... = actual Ruby code) or any kind of custom helper, like def initialize(**); do_init(**); end; (Sorry for "gravedigging", I'll open a separate issue, if that is more appropriate...)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I can't remember my exact reasoning, but it was more a limitation-in-my-capacity and not necessarily a limitation-by-design.

Copy link
Collaborator

Choose a reason for hiding this comment

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

@daniel-rikowski @zspencer can we please open up a new issue where this can be discussed further

Copy link
Member

Choose a reason for hiding this comment

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

👋 I'd also be interested in a PR with a failing test for the use case you want 😄

Choose a reason for hiding this comment

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

See #2020

@zspencer It seems this was because of a Ruby 3.1 bug 😉

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.

5 participants