Skip to content

Commit 2c19936

Browse files
Add from to use_helpers to add macro like syntax (#2034)
* 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 <[email protected]> Co-authored-by: Joel Hawksley <[email protected]>
1 parent e0dba0b commit 2c19936

12 files changed

+172
-16
lines changed

docs/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ nav_order: 5
2222

2323
*Reegan Viljoen*
2424

25+
* Add `from:` option to `use_helpers` to allow for more flexible helper inclusion from modules.
26+
27+
*Reegan Viljoen*
28+
2529
* Fixed ruby head matcher issue.
2630

2731
*Reegan Viljoen*

docs/adrs/0003-polymorphic-slot-definitions.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@ Here's how the `Item` sub-component of the list example above would be implement
5252

5353
```ruby
5454
class Item < ViewComponent::Base
55-
5655
renders_one :leading_visual, types: {
5756
icon: IconComponent, avatar: AvatarComponent
5857
}

docs/guide/helpers.md

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,16 +59,40 @@ By default, ViewComponents don't have access to helper methods defined externall
5959

6060
```ruby
6161
class UseHelpersComponent < ViewComponent::Base
62-
use_helpers :icon
62+
use_helpers :icon, :icon?
6363

6464
erb_template <<-ERB
6565
<div class="icon">
66-
<%= icon :user %>
66+
<%= icon? ? icon(:user) : icon(:guest) %>
6767
</div>
6868
ERB
6969
end
7070
```
7171

72+
Use the `from:` keyword to include individual methods defined in helper modules not available in the component:
73+
74+
```ruby
75+
class UserComponent < ViewComponent::Base
76+
use_helpers :icon, :icon?, from: IconHelper
77+
78+
def profile_icon
79+
icon? ? icon(:user) : icon(:guest)
80+
end
81+
end
82+
```
83+
84+
The singular version `use_helper` is also available:
85+
86+
```ruby
87+
class UserComponent < ViewComponent::Base
88+
use_helper :icon, from: IconHelper
89+
90+
def profile_icon
91+
icon :user
92+
end
93+
end
94+
```
95+
7296
## Nested URL helpers
7397

7498
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:

lib/view_component/instrumentation.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ def render_in(view_context, &block)
1616
identifier: self.class.identifier
1717
}
1818
) do
19-
super(view_context, &block)
19+
super
2020
end
2121
end
2222

lib/view_component/use_helpers.rb

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,27 @@ module ViewComponent::UseHelpers
44
extend ActiveSupport::Concern
55

66
class_methods do
7-
def use_helpers(*args)
8-
args.each do |helper_method|
9-
class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
10-
def #{helper_method}(*args, &block)
11-
raise HelpersCalledBeforeRenderError if view_context.nil?
12-
__vc_original_view_context.#{helper_method}(*args, &block)
13-
end
14-
RUBY
7+
def use_helpers(*args, from: nil)
8+
args.each { |helper_method| use_helper(helper_method, from: from) }
9+
end
10+
11+
def use_helper(helper_method, from: nil)
12+
class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
13+
def #{helper_method}(*args, &block)
14+
raise HelpersCalledBeforeRenderError if view_context.nil?
15+
16+
#{define_helper(helper_method: helper_method, source: from)}
17+
end
18+
RUBY
19+
ruby2_keywords(helper_method) if respond_to?(:ruby2_keywords, true)
20+
end
21+
22+
private
23+
24+
def define_helper(helper_method:, source:)
25+
return "__vc_original_view_context.#{helper_method}(*args, &block)" unless source.present?
1526

16-
ruby2_keywords(helper_method) if respond_to?(:ruby2_keywords, true)
17-
end
27+
"#{source}.instance_method(:#{helper_method}).bind(self).call(*args, &block)"
1828
end
1929
end
2030
end
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<div class='helper__message'>
2+
<%= message %>
3+
</div>
4+
5+
<div class='helper__args-message'>
6+
<%= message_with_args('macro helper method') %>
7+
</div>
8+
9+
<div class='helper__kwargs-message'>
10+
<%= message_with_kwargs(name: 'macro kwargs helper method') %>
11+
</div>
12+
13+
<div class='helper__block-message'>
14+
<%= block_content %>
15+
</div>
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# frozen_string_literal: true
2+
3+
class UseHelperMacroComponent < ViewComponent::Base
4+
use_helper :message, from: MacroHelper
5+
use_helper :message_with_args, from: MacroHelper
6+
use_helper :message_with_kwargs, from: MacroHelper
7+
use_helper :message_with_block, from: MacroHelper
8+
9+
def block_content
10+
message_with_block { "Hello block helper method" }
11+
end
12+
end
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# frozen_string_literal: true
22

33
class UseHelpersComponent < ViewComponent::Base
4-
use_helpers :message
4+
use_helper :message
55
end
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<div class='helper__message'>
2+
<%= message %>
3+
</div>
4+
5+
<div class='helper__args-message'>
6+
<%= message_with_args('macro helper method') %>
7+
</div>
8+
9+
<div class='helper__kwargs-message'>
10+
<%= message_with_kwargs(name: 'macro kwargs helper method') %>
11+
</div>
12+
13+
<div class='helper__block-message'>
14+
<%= block_content %>
15+
</div>
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# frozen_string_literal: true
2+
3+
class UseHelpersMacroComponent < ViewComponent::Base
4+
use_helpers :message, :message_with_args, :message_with_kwargs, :message_with_block, from: MacroHelper
5+
6+
def block_content
7+
message_with_block { "Hello block helper method" }
8+
end
9+
end
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# frozen_string_literal: true
2+
3+
module MacroHelper
4+
def message
5+
"Hello helper method"
6+
end
7+
8+
def message_with_args(name)
9+
"Hello #{name}"
10+
end
11+
12+
def message_with_kwargs(name:)
13+
"Hello #{name}"
14+
end
15+
16+
def message_with_block
17+
yield
18+
end
19+
end

test/sandbox/test/rendering_test.rb

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -563,7 +563,8 @@ def test_backtrace_returns_correct_file_and_line_number
563563
render_inline(ExceptionInTemplateComponent.new)
564564
end
565565

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

569570
def test_render_collection
@@ -1121,4 +1122,52 @@ def test_inline_component_renders_without_trailing_whitespace
11211122

11221123
refute @rendered_content =~ /\s+\z/, "Rendered component contains trailing whitespace"
11231124
end
1125+
1126+
def test_use_helpers_macros
1127+
render_inline(UseHelpersMacroComponent.new)
1128+
1129+
assert_selector ".helper__message", text: "Hello helper method"
1130+
end
1131+
1132+
def test_use_helpers_macros_with_args
1133+
render_inline(UseHelpersMacroComponent.new)
1134+
1135+
assert_selector ".helper__args-message", text: "Hello macro helper method"
1136+
end
1137+
1138+
def test_use_helpers_macros_with_kwargs
1139+
render_inline(UseHelpersMacroComponent.new)
1140+
1141+
assert_selector ".helper__kwargs-message", text: "Hello macro kwargs helper method"
1142+
end
1143+
1144+
def test_use_helpers_with_block
1145+
render_inline(UseHelpersMacroComponent.new)
1146+
1147+
assert_selector ".helper__block-message", text: "Hello block helper method"
1148+
end
1149+
1150+
def test_use_helper_macros
1151+
render_inline(UseHelperMacroComponent.new)
1152+
1153+
assert_selector ".helper__message", text: "Hello helper method"
1154+
end
1155+
1156+
def test_use_helper_macros_with_args
1157+
render_inline(UseHelperMacroComponent.new)
1158+
1159+
assert_selector ".helper__args-message", text: "Hello macro helper method"
1160+
end
1161+
1162+
def test_use_helper_macros_with_kwargs
1163+
render_inline(UseHelperMacroComponent.new)
1164+
1165+
assert_selector ".helper__kwargs-message", text: "Hello macro kwargs helper method"
1166+
end
1167+
1168+
def test_use_helper_macros_with_block
1169+
render_inline(UseHelperMacroComponent.new)
1170+
1171+
assert_selector ".helper__block-message", text: "Hello block helper method"
1172+
end
11241173
end

0 commit comments

Comments
 (0)