diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
index c32e80083..7138d806d 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -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*
diff --git a/docs/adrs/0003-polymorphic-slot-definitions.md b/docs/adrs/0003-polymorphic-slot-definitions.md
index 470c7de8d..2079290c9 100644
--- a/docs/adrs/0003-polymorphic-slot-definitions.md
+++ b/docs/adrs/0003-polymorphic-slot-definitions.md
@@ -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
}
diff --git a/docs/guide/helpers.md b/docs/guide/helpers.md
index 495277f2b..99a6f2520 100644
--- a/docs/guide/helpers.md
+++ b/docs/guide/helpers.md
@@ -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
- <%= icon :user %>
+ <%= icon? ? icon(:user) : icon(:guest) %>
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:
diff --git a/lib/view_component/instrumentation.rb b/lib/view_component/instrumentation.rb
index 380dea016..d63efd283 100644
--- a/lib/view_component/instrumentation.rb
+++ b/lib/view_component/instrumentation.rb
@@ -16,7 +16,7 @@ def render_in(view_context, &block)
identifier: self.class.identifier
}
) do
- super(view_context, &block)
+ super
end
end
diff --git a/lib/view_component/use_helpers.rb b/lib/view_component/use_helpers.rb
index 32d13cd43..61e6bd515 100644
--- a/lib/view_component/use_helpers.rb
+++ b/lib/view_component/use_helpers.rb
@@ -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
diff --git a/test/sandbox/app/components/use_helper_macro_component.html copy.erb b/test/sandbox/app/components/use_helper_macro_component.html copy.erb
new file mode 100644
index 000000000..0744510f4
--- /dev/null
+++ b/test/sandbox/app/components/use_helper_macro_component.html copy.erb
@@ -0,0 +1,15 @@
+
+ <%= message %>
+
+
+
+ <%= message_with_args('macro helper method') %>
+
+
+
+ <%= message_with_kwargs(name: 'macro kwargs helper method') %>
+
+
+
+ <%= block_content %>
+
diff --git a/test/sandbox/app/components/use_helper_macro_component.rb b/test/sandbox/app/components/use_helper_macro_component.rb
new file mode 100644
index 000000000..ff1822019
--- /dev/null
+++ b/test/sandbox/app/components/use_helper_macro_component.rb
@@ -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
diff --git a/test/sandbox/app/components/use_helpers_component.rb b/test/sandbox/app/components/use_helpers_component.rb
index a83bd2e16..752e83f0c 100644
--- a/test/sandbox/app/components/use_helpers_component.rb
+++ b/test/sandbox/app/components/use_helpers_component.rb
@@ -1,5 +1,5 @@
# frozen_string_literal: true
class UseHelpersComponent < ViewComponent::Base
- use_helpers :message
+ use_helper :message
end
diff --git a/test/sandbox/app/components/use_helpers_macro_component.html.erb b/test/sandbox/app/components/use_helpers_macro_component.html.erb
new file mode 100644
index 000000000..0744510f4
--- /dev/null
+++ b/test/sandbox/app/components/use_helpers_macro_component.html.erb
@@ -0,0 +1,15 @@
+
+ <%= message %>
+
+
+
+ <%= message_with_args('macro helper method') %>
+
+
+
+ <%= message_with_kwargs(name: 'macro kwargs helper method') %>
+
+
+
+ <%= block_content %>
+
diff --git a/test/sandbox/app/components/use_helpers_macro_component.rb b/test/sandbox/app/components/use_helpers_macro_component.rb
new file mode 100644
index 000000000..861cd36fd
--- /dev/null
+++ b/test/sandbox/app/components/use_helpers_macro_component.rb
@@ -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
diff --git a/test/sandbox/app/helpers/macro_helper.rb b/test/sandbox/app/helpers/macro_helper.rb
new file mode 100644
index 000000000..89b63b32d
--- /dev/null
+++ b/test/sandbox/app/helpers/macro_helper.rb
@@ -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
diff --git a/test/sandbox/test/rendering_test.rb b/test/sandbox/test/rendering_test.rb
index 9dd3ea21f..619c7f164 100644
--- a/test/sandbox/test/rendering_test.rb
+++ b/test/sandbox/test/rendering_test.rb
@@ -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
@@ -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