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