Skip to content

Commit

Permalink
Add support for multiple templates.
Browse files Browse the repository at this point in the history
Co-authored-by: Rob Sterner <[email protected]>
  • Loading branch information
2 people authored and jsolas committed Oct 13, 2021
1 parent f348d39 commit 4e864d5
Show file tree
Hide file tree
Showing 8 changed files with 116 additions and 4 deletions.
4 changes: 4 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,10 @@ title: Changelog

*Joel Hawksley*

* Add support for multiple templates.

*Rob Sterner*, *Joel Hawksley*

## 2.36.0

* Add `slot_type` helper method.
Expand Down
33 changes: 33 additions & 0 deletions docs/guide/templates.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,36 @@ Component subclasses inherit the parent component's template if they don't defin
class MyLinkComponent < LinkComponent
end
```

#### Multiple templates

ViewComponents can render multiple templates defined in the sidecar directory:

```
app/components
├── ...
├── test_component.rb
├── test_component
| ├── list.html.erb
| └── summary.html.erb
├── ...
```

Templates are compiled to methods in the format `call_#{template_basename}`, which can then be called in the component.

```ruby
class TestComponent < ViewComponent::Base
def initialize(mode:)
@mode = mode
end

def call
case @mode
when :list
call_list
when :summary
call_summary
end
end
end
```
2 changes: 1 addition & 1 deletion lib/view_component/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,7 @@ def _sidecar_files(extensions)
# view files in the same directory as the component
sidecar_files = Dir["#{directory}/#{component_name}.*{#{extensions}}"]

sidecar_directory_files = Dir["#{directory}/#{component_name}/#{filename}.*{#{extensions}}"]
sidecar_directory_files = Dir["#{directory}/#{component_name}/*.*{#{extensions}}"]

(sidecar_files - [source_location] + sidecar_directory_files + nested_component_files).uniq
end
Expand Down
32 changes: 29 additions & 3 deletions lib/view_component/compiler.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,18 @@ def compile(raise_errors: false)
templates.each do |template|
# Remove existing compiled template methods,
# as Ruby warns when redefining a method.
method_name = call_method_name(template[:variant])
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)
Expand Down Expand Up @@ -98,7 +109,14 @@ def template_errors
errors << "Could not find a template file or inline render method for #{component_class}."
end

if templates.count { |template| template[:variant].nil? } > 1
# Ensure that template base names are unique
# for each variant
unique_templates =
templates.map do |template|
template[:base_name] + template[:variant].to_s
end

if unique_templates.length != unique_templates.uniq.length
errors <<
"More than one template found for #{component_class}. " \
"There can only be one default template file per component."
Expand All @@ -118,7 +136,14 @@ def template_errors
"There can only be one template file per variant."
end

if templates.find { |template| template[:variant].nil? } && inline_calls_defined_on_self.include?(:call)
default_template_exists =
templates.find do |template|
pieces = File.basename(template[:path]).split(".")

template[:variant].nil? && pieces.first == component_class.name.demodulize.underscore
end

if default_template_exists && inline_calls_defined_on_self.include?(:call)
errors <<
"Template file and inline render method found for #{component_class}. " \
"There can only be a template file or inline render method per component."
Expand Down Expand Up @@ -151,6 +176,7 @@ def templates
pieces = File.basename(path).split(".")
memo << {
path: path,
base_name: path.split(File::SEPARATOR).last.split(".").first,
variant: pieces.second.split("+").second&.to_sym,
handler: pieces.last
}
Expand Down
18 changes: 18 additions & 0 deletions test/sandbox/app/components/multiple_templates_component.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# frozen_string_literal: true

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

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

def call
case @mode
when :list
call_list
when :summary
call_summary
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<ul>
<% @items.each do |item| %>
<li><%= item %></li>
<% end %>
</ul>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div>The items are: <%= @items.to_sentence %></div>
25 changes: 25 additions & 0 deletions test/view_component/view_component_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -776,6 +776,18 @@ def test_collection_component_with_trailing_comma_attr_reader
assert_match(/ProductReaderOopsComponent initializer is empty or invalid/, exception.message)
end

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

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")
end

def test_renders_component_using_rails_config
render_inline(RailsConfigComponent.new)

Expand Down Expand Up @@ -888,4 +900,17 @@ def test_output_postamble

assert_text("Hello, World!")
end

private

def modify_file(file, content)
filename = Rails.root.join(file)
old_content = File.read(filename)
begin
File.open(filename, "wb+") { |f| f.write(content) }
yield
ensure
File.open(filename, "wb+") { |f| f.write(old_content) }
end
end
end

0 comments on commit 4e864d5

Please sign in to comment.