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

Add from to use_helpers to add macro like syntax #2034

Merged
merged 23 commits into from
Jun 20, 2024
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
```

Methods defined in helper modules that aren't available in the component can be included individually using the `from:` keyword:
joelhawksley marked this conversation as resolved.
Show resolved Hide resolved

```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 for a single helper:
joelhawksley marked this conversation as resolved.
Show resolved Hide resolved

```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
Loading