Skip to content

Commit 77ccbac

Browse files
committed
Add ability to define templates that take parameters
```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 ```
1 parent a2fb384 commit 77ccbac

File tree

9 files changed

+121
-33
lines changed

9 files changed

+121
-33
lines changed

docs/guide/templates.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,4 +126,26 @@ class TestComponent < ViewComponent::Base
126126
end
127127
end
128128
end
129+
```
130+
131+
You can define templates that take parameters, using the `template_arguments` method.
132+
133+
```ruby
134+
class TestComponent < ViewComponent::Base
135+
template_arguments :list, :multiple # call_list now takes a `multiple` keyword argument
136+
def initialize(mode:)
137+
@mode = mode
138+
end
139+
140+
def call
141+
case @mode
142+
when :list
143+
call_list multiple: false
144+
when :multilist
145+
call_list multiple: true
146+
when :summary
147+
call_summary
148+
end
149+
end
150+
end
129151
```

lib/view_component/base.rb

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,26 @@ def compiler
393393
@__vc_compiler ||= Compiler.new(self)
394394
end
395395

396+
# Contains the arguments for additional templates
397+
# @return [Hash{String => Array<Symbol>}]
398+
# @private
399+
def template_arguments_hash
400+
@template_arguments_hash ||= {}
401+
end
402+
403+
# Declares arguments that need to be passed to a template.
404+
#
405+
# If a sidecar template needs additional locals that need to be passed
406+
# at call site, then this method should be used.
407+
#
408+
# The signature for the resulting method will use keyword arguments
409+
#
410+
# @param [Symbol, String] template The template that needs arguments
411+
# @param [Array<Symbol>] *args The arguments for the template
412+
def template_arguments(template, *args)
413+
compiler.add_template_arguments(template, *args)
414+
end
415+
396416
# we'll eventually want to update this to support other types
397417
# @private
398418
def type

lib/view_component/compiler.rb

Lines changed: 36 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -41,29 +41,21 @@ def compile(raise_errors: false)
4141
end
4242

4343
templates.each do |template|
44+
method_name, method_args = method_name_and_args_for template
45+
4446
# Remove existing compiled template methods,
4547
# as Ruby warns when redefining a method.
46-
pieces = File.basename(template[:path]).split(".")
47-
48-
method_name =
49-
# If the template matches the name of the component,
50-
# set the method name with call_method_name
51-
if pieces.first == component_class.name.demodulize.underscore
52-
call_method_name(template[:variant])
53-
# Otherwise, append the name of the template to
54-
# call_method_name
55-
else
56-
"#{call_method_name(template[:variant])}_#{pieces.first.to_sym}"
57-
end
58-
5948
if component_class.instance_methods.include?(method_name.to_sym)
6049
component_class.send(:undef_method, method_name.to_sym)
6150
end
6251

63-
component_class.class_eval <<-RUBY, template[:path], -1
64-
def #{method_name}
52+
component_class.class_eval <<-RUBY, template[:path], -2
53+
def #{method_name}(#{method_args.join(", ")})
54+
old_buffer = @output_buffer if defined? @output_buffer
6555
@output_buffer = ActionView::OutputBuffer.new
6656
#{compiled_template(template[:path])}
57+
ensure
58+
@output_buffer = old_buffer
6759
end
6860
RUBY
6961
end
@@ -75,10 +67,27 @@ def #{method_name}
7567
CompileCache.register(component_class)
7668
end
7769

70+
def add_template_arguments(template, *args)
71+
template = template.to_s
72+
raise ArgumentError, "Arguments already defined for template #{template}" if template_arguments.key?(template)
73+
74+
template_exists = templates.any? { |t| t[:base_name].split(".").first == template }
75+
raise ArgumentError, "Template does not exist: #{template}" unless template_exists
76+
77+
template_arguments[template] = args
78+
end
79+
7880
private
7981

8082
attr_reader :component_class
8183

84+
# Contains the arguments for additional templates
85+
# @return [Hash{String => Array<Symbol>}]
86+
# @private
87+
def template_arguments
88+
@template_arguments ||= {}
89+
end
90+
8291
def define_render_template_for
8392
if component_class.instance_methods.include?(:render_template_for)
8493
component_class.send(:undef_method, :render_template_for)
@@ -199,6 +208,18 @@ def inline_calls
199208
end
200209
end
201210

211+
def method_name_and_args_for(template)
212+
pieces = template[:base_name].split(".")
213+
if pieces.first == component_class.name.demodulize.underscore
214+
[call_method_name(template[:variant]), []]
215+
else
216+
[
217+
"#{call_method_name(template[:variant])}_#{pieces.first.to_sym}",
218+
(template_arguments || {}).fetch(pieces.first, []).map { |arg| "#{arg}:" }
219+
]
220+
end
221+
end
222+
202223
def inline_calls_defined_on_self
203224
@inline_calls_defined_on_self ||= component_class.instance_methods(false).grep(/^call/)
204225
end
Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,10 @@
11
# frozen_string_literal: true
22

33
class MultipleTemplatesComponent < ViewComponent::Base
4-
def initialize(mode:)
5-
@mode = mode
6-
4+
def initialize
75
@items = ["Apple", "Banana", "Pear"]
86
end
97

10-
def call
11-
case @mode
12-
when :list
13-
call_list
14-
when :summary
15-
call_summary
16-
end
17-
end
8+
template_arguments :list, :number
9+
template_arguments :summary, :string
1810
end

test/sandbox/app/components/multiple_templates_component/list.html.erb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<ul>
1+
<ul data-number="<%= number %>">
22
<% @items.each do |item| %>
33
<li><%= item %></li>
44
<% end %>
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<div class="container">
2+
<%= call_summary string: "foo" %>
3+
<%= call_list number: 1 %>
4+
</div>
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
<div>The items are: <%= @items.to_sentence %></div>
1+
<div>The items are: <%= @items.to_sentence %>, <%= string %></div>

test/view_component/base_test.rb

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,4 +77,35 @@ def test_sidecar_files
7777
TranslatableComponent._sidecar_files(["yml"])
7878
)
7979
end
80+
81+
def test_template_arguments_validates_existence
82+
error = assert_raises ArgumentError do
83+
Class.new(ViewComponent::Base) do
84+
def self._sidecar_files(*)
85+
[
86+
"/Users/fake.user/path/to.templates/component/test_component/test_component.html.erb",
87+
"/Users/fake.user/path/to.templates/component/test_component/sidecar.html.erb",
88+
]
89+
end
90+
template_arguments :non_existing, [:foo]
91+
end
92+
end
93+
assert_equal "Template does not exist: non_existing", error.message
94+
end
95+
96+
def test_template_arguments_validates_duplicates
97+
error = assert_raises ArgumentError do
98+
Class.new(ViewComponent::Base) do
99+
def self._sidecar_files(*)
100+
[
101+
"/Users/fake.user/path/to.templates/component/test_component/test_component.html.erb",
102+
"/Users/fake.user/path/to.templates/component/test_component/sidecar.html.erb",
103+
]
104+
end
105+
template_arguments :sidecar, [:foo]
106+
template_arguments :sidecar, [:bar]
107+
end
108+
end
109+
assert_equal "Arguments already defined for template sidecar", error.message
110+
end
80111
end

test/view_component/view_component_test.rb

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -749,15 +749,13 @@ def test_collection_component_with_trailing_comma_attr_reader
749749
end
750750

751751
def test_render_multiple_templates
752-
render_inline(MultipleTemplatesComponent.new(mode: :list))
752+
render_inline(MultipleTemplatesComponent.new)
753753

754+
assert_selector("div", text: "The items are: Apple, Banana, and Pear, foo")
754755
assert_selector("li", text: "Apple")
755756
assert_selector("li", text: "Banana")
756757
assert_selector("li", text: "Pear")
757-
758-
render_inline(MultipleTemplatesComponent.new(mode: :summary))
759-
760-
assert_selector("div", text: "Apple, Banana, and Pear")
758+
assert_selector("div.container")
761759
end
762760

763761
def test_renders_component_using_rails_config

0 commit comments

Comments
 (0)