Skip to content

Commit

Permalink
Add ability to define templates that take parameters
Browse files Browse the repository at this point in the history
```ruby
class TestComponent < ViewComponent::Base
  template_arguments :list, :multiple # call_list now takes a `multiple` keyword argument
  def initialize(mode:)
    @mode = mode
  end

  def call
    case @mode
    when :list
      call_list multiple: false
    when :multilist
      call_list multiple: true
    when :summary
      call_summary
    end
  end
end
```
  • Loading branch information
fsateler committed Sep 7, 2021
1 parent a2fb384 commit 77ccbac
Show file tree
Hide file tree
Showing 9 changed files with 121 additions and 33 deletions.
22 changes: 22 additions & 0 deletions docs/guide/templates.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,4 +126,26 @@ class TestComponent < ViewComponent::Base
end
end
end
```

You can define templates that take parameters, using the `template_arguments` method.

```ruby
class TestComponent < ViewComponent::Base
template_arguments :list, :multiple # call_list now takes a `multiple` keyword argument
def initialize(mode:)
@mode = mode
end

def call
case @mode
when :list
call_list multiple: false
when :multilist
call_list multiple: true
when :summary
call_summary
end
end
end
```
20 changes: 20 additions & 0 deletions lib/view_component/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,26 @@ def compiler
@__vc_compiler ||= Compiler.new(self)
end

# Contains the arguments for additional templates
# @return [Hash{String => Array<Symbol>}]
# @private
def template_arguments_hash
@template_arguments_hash ||= {}
end

# Declares arguments that need to be passed to a template.
#
# If a sidecar template needs additional locals that need to be passed
# at call site, then this method should be used.
#
# The signature for the resulting method will use keyword arguments
#
# @param [Symbol, String] template The template that needs arguments
# @param [Array<Symbol>] *args The arguments for the template
def template_arguments(template, *args)
compiler.add_template_arguments(template, *args)
end

# we'll eventually want to update this to support other types
# @private
def type
Expand Down
51 changes: 36 additions & 15 deletions lib/view_component/compiler.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,29 +41,21 @@ def compile(raise_errors: false)
end

templates.each do |template|
method_name, method_args = method_name_and_args_for template

# Remove existing compiled template methods,
# as Ruby warns when redefining a method.
pieces = File.basename(template[:path]).split(".")

method_name =
# If the template matches the name of the component,
# set the method name with call_method_name
if pieces.first == component_class.name.demodulize.underscore
call_method_name(template[:variant])
# Otherwise, append the name of the template to
# call_method_name
else
"#{call_method_name(template[:variant])}_#{pieces.first.to_sym}"
end

if component_class.instance_methods.include?(method_name.to_sym)
component_class.send(:undef_method, method_name.to_sym)
end

component_class.class_eval <<-RUBY, template[:path], -1
def #{method_name}
component_class.class_eval <<-RUBY, template[:path], -2
def #{method_name}(#{method_args.join(", ")})
old_buffer = @output_buffer if defined? @output_buffer
@output_buffer = ActionView::OutputBuffer.new
#{compiled_template(template[:path])}
ensure
@output_buffer = old_buffer
end
RUBY
end
Expand All @@ -75,10 +67,27 @@ def #{method_name}
CompileCache.register(component_class)
end

def add_template_arguments(template, *args)
template = template.to_s
raise ArgumentError, "Arguments already defined for template #{template}" if template_arguments.key?(template)

template_exists = templates.any? { |t| t[:base_name].split(".").first == template }
raise ArgumentError, "Template does not exist: #{template}" unless template_exists

template_arguments[template] = args
end

private

attr_reader :component_class

# Contains the arguments for additional templates
# @return [Hash{String => Array<Symbol>}]
# @private
def template_arguments
@template_arguments ||= {}
end

def define_render_template_for
if component_class.instance_methods.include?(:render_template_for)
component_class.send(:undef_method, :render_template_for)
Expand Down Expand Up @@ -199,6 +208,18 @@ def inline_calls
end
end

def method_name_and_args_for(template)
pieces = template[:base_name].split(".")
if pieces.first == component_class.name.demodulize.underscore
[call_method_name(template[:variant]), []]
else
[
"#{call_method_name(template[:variant])}_#{pieces.first.to_sym}",
(template_arguments || {}).fetch(pieces.first, []).map { |arg| "#{arg}:" }
]
end
end

def inline_calls_defined_on_self
@inline_calls_defined_on_self ||= component_class.instance_methods(false).grep(/^call/)
end
Expand Down
14 changes: 3 additions & 11 deletions test/sandbox/app/components/multiple_templates_component.rb
Original file line number Diff line number Diff line change
@@ -1,18 +1,10 @@
# frozen_string_literal: true

class MultipleTemplatesComponent < ViewComponent::Base
def initialize(mode:)
@mode = mode

def initialize
@items = ["Apple", "Banana", "Pear"]
end

def call
case @mode
when :list
call_list
when :summary
call_summary
end
end
template_arguments :list, :number
template_arguments :summary, :string
end
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<ul>
<ul data-number="<%= number %>">
<% @items.each do |item| %>
<li><%= item %></li>
<% end %>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<div class="container">
<%= call_summary string: "foo" %>
<%= call_list number: 1 %>
</div>
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<div>The items are: <%= @items.to_sentence %></div>
<div>The items are: <%= @items.to_sentence %>, <%= string %></div>
31 changes: 31 additions & 0 deletions test/view_component/base_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -77,4 +77,35 @@ def test_sidecar_files
TranslatableComponent._sidecar_files(["yml"])
)
end

def test_template_arguments_validates_existence
error = assert_raises ArgumentError do
Class.new(ViewComponent::Base) do
def self._sidecar_files(*)
[
"/Users/fake.user/path/to.templates/component/test_component/test_component.html.erb",
"/Users/fake.user/path/to.templates/component/test_component/sidecar.html.erb",
]
end
template_arguments :non_existing, [:foo]
end
end
assert_equal "Template does not exist: non_existing", error.message
end

def test_template_arguments_validates_duplicates
error = assert_raises ArgumentError do
Class.new(ViewComponent::Base) do
def self._sidecar_files(*)
[
"/Users/fake.user/path/to.templates/component/test_component/test_component.html.erb",
"/Users/fake.user/path/to.templates/component/test_component/sidecar.html.erb",
]
end
template_arguments :sidecar, [:foo]
template_arguments :sidecar, [:bar]
end
end
assert_equal "Arguments already defined for template sidecar", error.message
end
end
8 changes: 3 additions & 5 deletions test/view_component/view_component_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -749,15 +749,13 @@ def test_collection_component_with_trailing_comma_attr_reader
end

def test_render_multiple_templates
render_inline(MultipleTemplatesComponent.new(mode: :list))
render_inline(MultipleTemplatesComponent.new)

assert_selector("div", text: "The items are: Apple, Banana, and Pear, foo")
assert_selector("li", text: "Apple")
assert_selector("li", text: "Banana")
assert_selector("li", text: "Pear")

render_inline(MultipleTemplatesComponent.new(mode: :summary))

assert_selector("div", text: "Apple, Banana, and Pear")
assert_selector("div.container")
end

def test_renders_component_using_rails_config
Expand Down

0 comments on commit 77ccbac

Please sign in to comment.