Skip to content

Commit

Permalink
Add from to use_helpers to add macro like syntax (#2034)
Browse files Browse the repository at this point in the history
* add: helpers macro

* refactor helpers macro

* refactor helpers macro

* add: kwargs tests

* add: documentation

* add: changelog entry

* add: block test and use_helper singular method

* fix: ruby2_keywords warnings

* add: singular documentation

* fix: linting

* fix: linting

* fix: lint

* refactor: code

* fix rails main tests

* fix: linting

* fix linting

* fix: linting

* Apply suggestions from code review

---------

Co-authored-by: Joel Hawksley <joel@hawksley.org>
Co-authored-by: Joel Hawksley <joelhawksley@github.com>
  • Loading branch information
3 people authored Jun 20, 2024
1 parent e0dba0b commit 2c19936
Show file tree
Hide file tree
Showing 12 changed files with 172 additions and 16 deletions.
4 changes: 4 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ nav_order: 5

*Reegan Viljoen*

* Add `from:` option to `use_helpers` to allow for more flexible helper inclusion from modules.

*Reegan Viljoen*

* Fixed ruby head matcher issue.

*Reegan Viljoen*
Expand Down
1 change: 0 additions & 1 deletion docs/adrs/0003-polymorphic-slot-definitions.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@ Here's how the `Item` sub-component of the list example above would be implement

```ruby
class Item < ViewComponent::Base

renders_one :leading_visual, types: {
icon: IconComponent, avatar: AvatarComponent
}
Expand Down
28 changes: 26 additions & 2 deletions docs/guide/helpers.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,16 +59,40 @@ By default, ViewComponents don't have access to helper methods defined externall

```ruby
class UseHelpersComponent < ViewComponent::Base
use_helpers :icon
use_helpers :icon, :icon?

erb_template <<-ERB
<div class="icon">
<%= icon :user %>
<%= icon? ? icon(:user) : icon(:guest) %>
</div>
ERB
end
```

Use the `from:` keyword to include individual methods defined in helper modules not available in the component:

```ruby
class UserComponent < ViewComponent::Base
use_helpers :icon, :icon?, from: IconHelper

def profile_icon
icon? ? icon(:user) : icon(:guest)
end
end
```

The singular version `use_helper` is also available:

```ruby
class UserComponent < ViewComponent::Base
use_helper :icon, from: IconHelper

def profile_icon
icon :user
end
end
```

## Nested URL helpers

Rails nested URL helpers implicitly depend on the current `request` in certain cases. Since ViewComponent is built to enable reusing components in different contexts, nested URL helpers should be passed their options explicitly:
Expand Down
2 changes: 1 addition & 1 deletion lib/view_component/instrumentation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def render_in(view_context, &block)
identifier: self.class.identifier
}
) do
super(view_context, &block)
super
end
end

Expand Down
30 changes: 20 additions & 10 deletions lib/view_component/use_helpers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,27 @@ module ViewComponent::UseHelpers
extend ActiveSupport::Concern

class_methods do
def use_helpers(*args)
args.each do |helper_method|
class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
def #{helper_method}(*args, &block)
raise HelpersCalledBeforeRenderError if view_context.nil?
__vc_original_view_context.#{helper_method}(*args, &block)
end
RUBY
def use_helpers(*args, from: nil)
args.each { |helper_method| use_helper(helper_method, from: from) }
end

def use_helper(helper_method, from: nil)
class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
def #{helper_method}(*args, &block)
raise HelpersCalledBeforeRenderError if view_context.nil?
#{define_helper(helper_method: helper_method, source: from)}
end
RUBY
ruby2_keywords(helper_method) if respond_to?(:ruby2_keywords, true)
end

private

def define_helper(helper_method:, source:)
return "__vc_original_view_context.#{helper_method}(*args, &block)" unless source.present?

ruby2_keywords(helper_method) if respond_to?(:ruby2_keywords, true)
end
"#{source}.instance_method(:#{helper_method}).bind(self).call(*args, &block)"
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<div class='helper__message'>
<%= message %>
</div>

<div class='helper__args-message'>
<%= message_with_args('macro helper method') %>
</div>

<div class='helper__kwargs-message'>
<%= message_with_kwargs(name: 'macro kwargs helper method') %>
</div>

<div class='helper__block-message'>
<%= block_content %>
</div>
12 changes: 12 additions & 0 deletions test/sandbox/app/components/use_helper_macro_component.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# frozen_string_literal: true

class UseHelperMacroComponent < ViewComponent::Base
use_helper :message, from: MacroHelper
use_helper :message_with_args, from: MacroHelper
use_helper :message_with_kwargs, from: MacroHelper
use_helper :message_with_block, from: MacroHelper

def block_content
message_with_block { "Hello block helper method" }
end
end
2 changes: 1 addition & 1 deletion test/sandbox/app/components/use_helpers_component.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# frozen_string_literal: true

class UseHelpersComponent < ViewComponent::Base
use_helpers :message
use_helper :message
end
15 changes: 15 additions & 0 deletions test/sandbox/app/components/use_helpers_macro_component.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<div class='helper__message'>
<%= message %>
</div>

<div class='helper__args-message'>
<%= message_with_args('macro helper method') %>
</div>

<div class='helper__kwargs-message'>
<%= message_with_kwargs(name: 'macro kwargs helper method') %>
</div>

<div class='helper__block-message'>
<%= block_content %>
</div>
9 changes: 9 additions & 0 deletions test/sandbox/app/components/use_helpers_macro_component.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# frozen_string_literal: true

class UseHelpersMacroComponent < ViewComponent::Base
use_helpers :message, :message_with_args, :message_with_kwargs, :message_with_block, from: MacroHelper

def block_content
message_with_block { "Hello block helper method" }
end
end
19 changes: 19 additions & 0 deletions test/sandbox/app/helpers/macro_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# frozen_string_literal: true

module MacroHelper
def message
"Hello helper method"
end

def message_with_args(name)
"Hello #{name}"
end

def message_with_kwargs(name:)
"Hello #{name}"
end

def message_with_block
yield
end
end
51 changes: 50 additions & 1 deletion test/sandbox/test/rendering_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -563,7 +563,8 @@ def test_backtrace_returns_correct_file_and_line_number
render_inline(ExceptionInTemplateComponent.new)
end

assert_match %r{app/components/exception_in_template_component\.html\.erb:2}, error.backtrace.first
component_error_index = (Rails::VERSION::STRING < "8.0") ? 0 : 1
assert_match %r{app/components/exception_in_template_component\.html\.erb:2}, error.backtrace[component_error_index]
end

def test_render_collection
Expand Down Expand Up @@ -1121,4 +1122,52 @@ def test_inline_component_renders_without_trailing_whitespace

refute @rendered_content =~ /\s+\z/, "Rendered component contains trailing whitespace"
end

def test_use_helpers_macros
render_inline(UseHelpersMacroComponent.new)

assert_selector ".helper__message", text: "Hello helper method"
end

def test_use_helpers_macros_with_args
render_inline(UseHelpersMacroComponent.new)

assert_selector ".helper__args-message", text: "Hello macro helper method"
end

def test_use_helpers_macros_with_kwargs
render_inline(UseHelpersMacroComponent.new)

assert_selector ".helper__kwargs-message", text: "Hello macro kwargs helper method"
end

def test_use_helpers_with_block
render_inline(UseHelpersMacroComponent.new)

assert_selector ".helper__block-message", text: "Hello block helper method"
end

def test_use_helper_macros
render_inline(UseHelperMacroComponent.new)

assert_selector ".helper__message", text: "Hello helper method"
end

def test_use_helper_macros_with_args
render_inline(UseHelperMacroComponent.new)

assert_selector ".helper__args-message", text: "Hello macro helper method"
end

def test_use_helper_macros_with_kwargs
render_inline(UseHelperMacroComponent.new)

assert_selector ".helper__kwargs-message", text: "Hello macro kwargs helper method"
end

def test_use_helper_macros_with_block
render_inline(UseHelperMacroComponent.new)

assert_selector ".helper__block-message", text: "Hello block helper method"
end
end

0 comments on commit 2c19936

Please sign in to comment.