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 Aug 23, 2020
1 parent 78f3fab commit c2bbebb
Show file tree
Hide file tree
Showing 9 changed files with 96 additions and 20 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

* Add support for multiple templates.

*Rob Sterner*, *Joel Hawksley*
*Rob Sterner*, *Joel Hawksley*, *Felipe Sateler*

# 2.18.1

Expand Down
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,28 @@ class TestComponent < ViewComponent::Base
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
```

### Conditional Rendering

Components can implement a `#render?` method to be called after initialization to determine if the component should render.
Expand Down
2 changes: 1 addition & 1 deletion coverage/coverage.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ Incomplete test coverage
--------------------------------------------------------------------------------

/app/controllers/view_components_controller.rb: 98.18% (missed: 59)
/lib/view_component/base.rb: 97.87% (missed: 178,268,296,359)
/lib/view_component/base.rb: 98.05% (missed: 178,268,326,389)
/lib/view_component/engine.rb: 96.43% (missed: 22,25)
/lib/view_component/preview.rb: 94.12% (missed: 49,59,109)
/lib/view_component/render_to_string_monkey_patch.rb: 83.33% (missed: 9)
Expand Down
47 changes: 32 additions & 15 deletions lib/view_component/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -271,26 +271,14 @@ 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 == 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

undef_method(method_name.to_sym) if instance_methods.include?(method_name.to_sym)

class_eval <<-RUBY, template[:path], line_number
def #{method_name}
def #{method_name}(#{method_args.join(", ")})
@output_buffer = ActionView::OutputBuffer.new
#{compiled_template(template[:path])}
end
Expand All @@ -300,6 +288,23 @@ def #{method_name}
CompileCache.register self
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)
@template_arguments ||= {}
template = template.to_s
raise ArgumentError, "Arguments already defined for template #{template}" if @template_arguments.key?(template)
raise ArgumentError, "Template does not exist: #{template}" if templates.none? { |t| t[:base_name].split(".").first == template }
@template_arguments[template] = args
end

# we'll eventually want to update this to support other types
def type
"text/html"
Expand Down Expand Up @@ -373,6 +378,18 @@ def compiled_template(file_path)
end
end

def method_name_and_args_for(template)
pieces = template[:base_name].split(".")
if pieces.first == 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
@inline_calls ||=
begin
Expand Down
7 changes: 5 additions & 2 deletions test/app/components/multiple_templates_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,15 @@ def initialize(mode:)
@items = ["Apple", "Banana", "Pear"]
end

template_arguments :list, :number
template_arguments :summary, :string

def call
case @mode
when :list
call_list
call_list number: 1
when :summary
call_summary
call_summary string: "foo"
end
end
end
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<ul>
<%= number %>
<% @items.each do |item| %>
<li><%= item %></li>
<% end %>
Expand Down
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 @@ -25,4 +25,35 @@ def test_templates_parses_all_types_of_paths
end
end
end

def test_template_arguments_validates_existence
error = assert_raises ArgumentError do
Class.new(ViewComponent::Base) do
def self.matching_views_in_source_location
[
"/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.matching_views_in_source_location
[
"/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
2 changes: 2 additions & 0 deletions test/view_component/view_component_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -646,10 +646,12 @@ def test_render_multiple_templates
assert_selector("li", text: "Apple")
assert_selector("li", text: "Banana")
assert_selector("li", text: "Pear")
assert_selector("ul", text: "1")

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

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

def test_renders_component_using_rails_config
Expand Down

0 comments on commit c2bbebb

Please sign in to comment.