actionview-component 1.7.0 → 1.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/.github/ISSUE_TEMPLATE +5 -8
  3. data/.github/PULL_REQUEST_TEMPLATE +2 -4
  4. data/.github/workflows/ruby_on_rails.yml +6 -2
  5. data/.gitignore +1 -0
  6. data/.rubocop.yml +4 -0
  7. data/CHANGELOG.md +150 -0
  8. data/CONTRIBUTING.md +10 -19
  9. data/Gemfile.lock +20 -5
  10. data/README.md +204 -248
  11. data/actionview-component.gemspec +10 -8
  12. data/{lib/railties/lib → app/controllers}/rails/components_controller.rb +18 -8
  13. data/docs/case-studies/jellyswitch.md +76 -0
  14. data/lib/action_view/component.rb +1 -20
  15. data/lib/action_view/component/base.rb +2 -246
  16. data/lib/action_view/component/preview.rb +1 -80
  17. data/lib/action_view/component/railtie.rb +1 -64
  18. data/lib/action_view/component/test_case.rb +1 -3
  19. data/lib/action_view/component/test_helpers.rb +1 -19
  20. data/lib/rails/generators/component/component_generator.rb +6 -12
  21. data/lib/rails/generators/component/templates/component.rb.tt +0 -4
  22. data/lib/rails/generators/erb/component_generator.rb +21 -0
  23. data/lib/rails/generators/erb/templates/component.html.erb.tt +1 -0
  24. data/lib/rails/generators/haml/component_generator.rb +21 -0
  25. data/lib/rails/generators/haml/templates/component.html.haml.tt +1 -0
  26. data/lib/rails/generators/rspec/component_generator.rb +1 -1
  27. data/lib/rails/generators/rspec/templates/component_spec.rb.tt +1 -1
  28. data/lib/rails/generators/slim/component_generator.rb +21 -0
  29. data/lib/rails/generators/slim/templates/component.html.slim.tt +1 -0
  30. data/lib/rails/generators/test_unit/component_generator.rb +1 -1
  31. data/lib/rails/generators/test_unit/templates/component_test.rb.tt +3 -3
  32. data/lib/railties/lib/rails.rb +0 -1
  33. data/lib/railties/lib/rails/templates/rails/components/preview.html.erb +1 -1
  34. data/lib/view_component.rb +30 -0
  35. data/lib/view_component/base.rb +279 -0
  36. data/lib/view_component/conversion.rb +9 -0
  37. data/lib/view_component/engine.rb +65 -0
  38. data/lib/view_component/preview.rb +78 -0
  39. data/lib/view_component/previewable.rb +25 -0
  40. data/lib/view_component/render_monkey_patch.rb +31 -0
  41. data/lib/view_component/rendering_monkey_patch.rb +13 -0
  42. data/lib/view_component/template_error.rb +9 -0
  43. data/lib/view_component/test_case.rb +9 -0
  44. data/lib/view_component/test_helpers.rb +39 -0
  45. data/lib/view_component/version.rb +11 -0
  46. data/script/console +1 -1
  47. metadata +48 -21
  48. data/lib/action_view/component/conversion.rb +0 -11
  49. data/lib/action_view/component/previewable.rb +0 -27
  50. data/lib/action_view/component/render_monkey_patch.rb +0 -29
  51. data/lib/action_view/component/template_error.rb +0 -11
  52. data/lib/action_view/component/version.rb +0 -13
  53. data/lib/rails/generators/component/templates/component.html.erb.tt +0 -5
  54. data/lib/railties/lib/rails/component_examples_controller.rb +0 -9
  55. data/lib/railties/lib/rails/templates/rails/examples/show.html.erb +0 -1
@@ -1,87 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "active_support/descendants_tracker"
4
-
5
3
  module ActionView
6
4
  module Component # :nodoc:
7
- class Preview
8
- extend ActiveSupport::DescendantsTracker
9
- include ActionView::Component::TestHelpers
10
-
11
- def render(component, *locals, &block)
12
- render_inline(component, *locals, &block)
13
- end
14
-
15
- class << self
16
- # Returns all component preview classes.
17
- def all
18
- load_previews if descendants.empty?
19
- descendants
20
- end
21
-
22
- # Returns the html of the component in its layout
23
- def call(example, layout: nil)
24
- example_html = new.public_send(example)
25
- if layout.nil?
26
- layout = @layout.nil? ? "layouts/application" : @layout
27
- end
28
-
29
- Rails::ComponentExamplesController.render(template: "examples/show",
30
- layout: layout,
31
- assigns: { example: example_html })
32
- end
33
-
34
- # Returns the component object class associated to the preview.
35
- def component
36
- self.name.sub(%r{Preview$}, "").constantize
37
- end
38
-
39
- # Returns all of the available examples for the component preview.
40
- def examples
41
- public_instance_methods(false).map(&:to_s).sort
42
- end
43
-
44
- # Returns +true+ if the example of the component preview exists.
45
- def example_exists?(example)
46
- examples.include?(example)
47
- end
48
-
49
- # Returns +true+ if the preview exists.
50
- def exists?(preview)
51
- all.any? { |p| p.preview_name == preview }
52
- end
53
-
54
- # Find a component preview by its underscored class name.
55
- def find(preview)
56
- all.find { |p| p.preview_name == preview }
57
- end
58
-
59
- # Returns the underscored name of the component preview without the suffix.
60
- def preview_name
61
- name.sub(/Preview$/, "").underscore
62
- end
63
-
64
- # Setter for layout name.
65
- def layout(layout_name)
66
- @layout = layout_name
67
- end
68
-
69
- private
70
-
71
- def load_previews
72
- if preview_path
73
- Dir["#{preview_path}/**/*_preview.rb"].sort.each { |file| require_dependency file }
74
- end
75
- end
76
-
77
- def preview_path
78
- Base.preview_path
79
- end
80
-
81
- def show_previews
82
- Base.show_previews
83
- end
84
- end
5
+ class Preview < ViewComponent::Preview
85
6
  end
86
7
  end
87
8
  end
@@ -1,66 +1,3 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "rails"
4
- require "action_view/component"
5
-
6
- module ActionView
7
- module Component
8
- class Railtie < Rails::Railtie # :nodoc:
9
- config.action_view_component = ActiveSupport::OrderedOptions.new
10
-
11
- initializer "action_view_component.set_configs" do |app|
12
- options = app.config.action_view_component
13
-
14
- options.show_previews = Rails.env.development? if options.show_previews.nil?
15
-
16
- if options.show_previews
17
- options.preview_path ||= defined?(Rails.root) ? "#{Rails.root}/test/components/previews" : nil
18
- end
19
-
20
- ActiveSupport.on_load(:action_view_component) do
21
- options.each { |k, v| send("#{k}=", v) }
22
- end
23
- end
24
-
25
- initializer "action_view_component.set_autoload_paths" do |app|
26
- require "railties/lib/rails/components_controller"
27
- require "railties/lib/rails/component_examples_controller"
28
-
29
- options = app.config.action_view_component
30
-
31
- if options.show_previews && options.preview_path
32
- ActiveSupport::Dependencies.autoload_paths << options.preview_path
33
- end
34
- end
35
-
36
- initializer "action_view_component.eager_load_actions" do
37
- ActiveSupport.on_load(:after_initialize) do
38
- ActionView::Component::Base.descendants.each(&:compile)
39
- end
40
- end
41
-
42
- initializer "action_view_component.compile_config_methods" do
43
- ActiveSupport.on_load(:action_view_component) do
44
- config.compile_methods! if config.respond_to?(:compile_methods!)
45
- end
46
- end
47
-
48
- initializer "action_view_component.monkey_patch_render" do
49
- ActiveSupport.on_load(:action_view) do
50
- ActionView::Base.prepend ActionView::Component::RenderMonkeyPatch
51
- end
52
- end
53
-
54
- config.after_initialize do |app|
55
- options = app.config.action_view_component
56
-
57
- if options.show_previews
58
- app.routes.prepend do
59
- get "/rails/components" => "rails/components#index", :internal => true
60
- get "/rails/components/*path" => "rails/components#previews", :internal => true
61
- end
62
- end
63
- end
64
- end
65
- end
66
- end
3
+ require "view_component/engine"
@@ -1,10 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "active_support/test_case"
4
-
5
3
  module ActionView
6
4
  module Component
7
- class TestCase < ActiveSupport::TestCase
5
+ class TestCase
8
6
  include ActionView::Component::TestHelpers
9
7
  end
10
8
  end
@@ -3,17 +3,7 @@
3
3
  module ActionView
4
4
  module Component
5
5
  module TestHelpers
6
- def render_inline(component, **args, &block)
7
- Nokogiri::HTML.fragment(controller.view_context.render(component, args, &block))
8
- end
9
-
10
- def controller
11
- @controller ||= ApplicationController.new.tap { |c| c.request = request }
12
- end
13
-
14
- def request
15
- @request ||= ActionDispatch::TestRequest.create
16
- end
6
+ include ViewComponent::TestHelpers
17
7
 
18
8
  def render_component(component, **args, &block)
19
9
  ActiveSupport::Deprecation.warn(
@@ -22,14 +12,6 @@ module ActionView
22
12
 
23
13
  render_inline(component, args, &block)
24
14
  end
25
-
26
- def with_variant(variant)
27
- old_variants = controller.view_context.lookup_context.variants
28
-
29
- controller.view_context.lookup_context.variants = variant
30
- yield
31
- controller.view_context.lookup_context.variants = old_variants
32
- end
33
15
  end
34
16
  end
35
17
  end
@@ -6,15 +6,16 @@ module Rails
6
6
  source_root File.expand_path("templates", __dir__)
7
7
 
8
8
  argument :attributes, type: :array, default: [], banner: "attribute"
9
- hook_for :test_framework
10
9
  check_class_collision suffix: "Component"
11
10
 
12
11
  def create_component_file
13
- template "component.rb", File.join("app/components", "#{file_name}_component.rb")
12
+ template "component.rb", File.join("app/components", class_path, "#{file_name}_component.rb")
14
13
  end
15
14
 
16
- def create_template_file
17
- template "component.html.erb", File.join("app/components", "#{file_name}_component.html.erb")
15
+ hook_for :test_framework
16
+
17
+ hook_for :template_engine do |instance, template_engine|
18
+ instance.invoke template_engine, [instance.name]
18
19
  end
19
20
 
20
21
  private
@@ -23,15 +24,8 @@ module Rails
23
24
  @_file_name ||= super.sub(/_component\z/i, "")
24
25
  end
25
26
 
26
- def requires_content?
27
- return @requires_content if @asked
28
-
29
- @asked = true
30
- @requires_content = ask("Would you like #{class_name} to require content? (Y/n)").downcase == "y"
31
- end
32
-
33
27
  def parent_class
34
- defined?(ApplicationComponent) ? "ApplicationComponent" : "ActionView::Component::Base"
28
+ defined?(ApplicationComponent) ? "ApplicationComponent" : "ViewComponent::Base"
35
29
  end
36
30
 
37
31
  def initialize_signature
@@ -1,8 +1,4 @@
1
1
  class <%= class_name %>Component < <%= parent_class %>
2
- <%- if requires_content? -%>
3
- validates :content, presence: true
4
- <%- end -%>
5
-
6
2
  def initialize(<%= initialize_signature %>)
7
3
  <%= initialize_body %>
8
4
  end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators/erb"
4
+
5
+ module Erb
6
+ module Generators
7
+ class ComponentGenerator < Base
8
+ source_root File.expand_path("templates", __dir__)
9
+
10
+ def copy_view_file
11
+ template "component.html.erb", File.join("app/components", class_path, "#{file_name}_component.html.erb")
12
+ end
13
+
14
+ private
15
+
16
+ def file_name
17
+ @_file_name ||= super.sub(/_component\z/i, "")
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1 @@
1
+ <div>Add <%= class_name %> template here</div>
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators/erb/component_generator"
4
+
5
+ module Haml
6
+ module Generators
7
+ class ComponentGenerator < Erb::Generators::ComponentGenerator
8
+ source_root File.expand_path("templates", __dir__)
9
+
10
+ def copy_view_file
11
+ template "component.html.haml", File.join("app/components", class_path, "#{file_name}_component.html.haml")
12
+ end
13
+
14
+ private
15
+
16
+ def file_name
17
+ @_file_name ||= super.sub(/_component\z/i, "")
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1 @@
1
+ %div Add <%= class_name %> template here
@@ -6,7 +6,7 @@ module Rspec
6
6
  source_root File.expand_path("templates", __dir__)
7
7
 
8
8
  def create_test_file
9
- template "component_spec.rb", File.join("spec/components", "#{file_name}_component_spec.rb")
9
+ template "component_spec.rb", File.join("spec/components", class_path, "#{file_name}_component_spec.rb")
10
10
  end
11
11
 
12
12
  private
@@ -5,7 +5,7 @@ RSpec.describe <%= class_name %>Component, type: :component do
5
5
 
6
6
  # it "renders something useful" do
7
7
  # expect(
8
- # render_inline(described_class, attr: "value") { "Hello, components!" }.css("p").to_html
8
+ # render_inline(described_class.new(attr: "value")) { "Hello, components!" }.css("p").to_html
9
9
  # ).to include(
10
10
  # "Hello, components!"
11
11
  # )
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators/erb/component_generator"
4
+
5
+ module Slim
6
+ module Generators
7
+ class ComponentGenerator < Erb::Generators::ComponentGenerator
8
+ source_root File.expand_path("templates", __dir__)
9
+
10
+ def copy_view_file
11
+ template "component.html.slim", File.join("app/components", class_path, "#{file_name}_component.html.slim")
12
+ end
13
+
14
+ private
15
+
16
+ def file_name
17
+ @_file_name ||= super.sub(/_component\z/i, "")
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1 @@
1
+ div Add <%= class_name %> template here
@@ -7,7 +7,7 @@ module TestUnit
7
7
  check_class_collision suffix: "ComponentTest"
8
8
 
9
9
  def create_test_file
10
- template "component_test.rb", File.join("test/components", "#{file_name}_component_test.rb")
10
+ template "component_test.rb", File.join("test/components", class_path, "#{file_name}_component_test.rb")
11
11
  end
12
12
 
13
13
  private
@@ -1,10 +1,10 @@
1
1
  require "test_helper"
2
2
 
3
- class <%= class_name %>ComponentTest < ActionView::Component::TestCase
3
+ class <%= class_name %>ComponentTest < ViewComponent::TestCase
4
4
  test "component renders something useful" do
5
5
  # assert_equal(
6
- # %(<span title="my title">Hello, components!</span>),
7
- # render_inline(<%= class_name %>Component, attr: "value") { "Hello, components!" }.css("span").to_html
6
+ # %(<span>Hello, components!</span>),
7
+ # render_inline(<%= class_name %>Component.new(message: "Hello, components!")).css("span").to_html
8
8
  # )
9
9
  end
10
10
  end
@@ -2,5 +2,4 @@
2
2
 
3
3
  module Rails
4
4
  autoload :ComponentsController
5
- autoload :ComponentExamplesController
6
5
  end
@@ -1 +1 @@
1
- <%= raw @preview.call(@example_name) %>
1
+ <%= render(@render_args[:component], **@render_args[:args], &@render_args[:block])%>
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+ require "action_view"
3
+ require "active_support/dependencies/autoload"
4
+
5
+ module ViewComponent
6
+ extend ActiveSupport::Autoload
7
+
8
+ autoload :Base
9
+ autoload :Conversion
10
+ autoload :Preview
11
+ autoload :Previewable
12
+ autoload :TestHelpers
13
+ autoload :TestCase
14
+ autoload :RenderMonkeyPatch
15
+ autoload :RenderingMonkeyPatch
16
+ autoload :TemplateError
17
+ end
18
+
19
+ module ActionView
20
+ module Component
21
+ extend ActiveSupport::Autoload
22
+
23
+ autoload :Base
24
+ autoload :Preview
25
+ autoload :TestCase
26
+ autoload :TestHelpers
27
+ end
28
+ end
29
+
30
+ ActiveModel::Conversion.include ViewComponent::Conversion
@@ -0,0 +1,279 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "action_view"
4
+ require "active_support/configurable"
5
+ require "view_component/previewable"
6
+
7
+ module ViewComponent
8
+ class Base < ActionView::Base
9
+ include ActiveSupport::Configurable
10
+ include ViewComponent::Previewable
11
+
12
+ delegate :form_authenticity_token, :protect_against_forgery?, to: :helpers
13
+
14
+ class_attribute :content_areas, default: []
15
+ self.content_areas = [] # default doesn't work until Rails 5.2
16
+
17
+ # Entrypoint for rendering components.
18
+ #
19
+ # view_context: ActionView context from calling view
20
+ # block: optional block to be captured within the view context
21
+ #
22
+ # returns HTML that has been escaped by the respective template handler
23
+ #
24
+ # Example subclass:
25
+ #
26
+ # app/components/my_component.rb:
27
+ # class MyComponent < ViewComponent::Base
28
+ # def initialize(title:)
29
+ # @title = title
30
+ # end
31
+ # end
32
+ #
33
+ # app/components/my_component.html.erb
34
+ # <span title="<%= @title %>">Hello, <%= content %>!</span>
35
+ #
36
+ # In use:
37
+ # <%= render MyComponent.new(title: "greeting") do %>world<% end %>
38
+ # returns:
39
+ # <span title="greeting">Hello, world!</span>
40
+ #
41
+ def render_in(view_context, &block)
42
+ self.class.compile!
43
+ @view_context = view_context
44
+ @view_renderer ||= view_context.view_renderer
45
+ @lookup_context ||= view_context.lookup_context
46
+ @view_flow ||= view_context.view_flow
47
+ @virtual_path ||= virtual_path
48
+ @variant = @lookup_context.variants.first
49
+
50
+ old_current_template = @current_template
51
+ @current_template = self
52
+
53
+ @content = view_context.capture(self, &block) if block_given?
54
+
55
+ before_render_check
56
+
57
+ if render?
58
+ send(self.class.call_method_name(@variant))
59
+ else
60
+ ""
61
+ end
62
+ ensure
63
+ @current_template = old_current_template
64
+ end
65
+
66
+ def before_render_check
67
+ # noop
68
+ end
69
+
70
+ def render?
71
+ true
72
+ end
73
+
74
+ def initialize(*); end
75
+
76
+ def render(options = {}, args = {}, &block)
77
+ if options.is_a?(String) || (options.is_a?(Hash) && options.has_key?(:partial))
78
+ view_context.render(options, args, &block)
79
+ else
80
+ super
81
+ end
82
+ end
83
+
84
+ def controller
85
+ @controller ||= view_context.controller
86
+ end
87
+
88
+ # Provides a proxy to access helper methods through
89
+ def helpers
90
+ @helpers ||= view_context
91
+ end
92
+
93
+ # Removes the first part of the path and the extension.
94
+ def virtual_path
95
+ self.class.source_location.gsub(%r{(.*app/components)|(\.rb)}, "")
96
+ end
97
+
98
+ def view_cache_dependencies
99
+ []
100
+ end
101
+
102
+ def format # :nodoc:
103
+ @variant
104
+ end
105
+
106
+ def with(area, content = nil, &block)
107
+ unless content_areas.include?(area)
108
+ raise ArgumentError.new "Unknown content_area '#{area}' - expected one of '#{content_areas}'"
109
+ end
110
+
111
+ if block_given?
112
+ content = view_context.capture(&block)
113
+ end
114
+
115
+ instance_variable_set("@#{area}".to_sym, content)
116
+ nil
117
+ end
118
+
119
+ private
120
+
121
+ def request
122
+ @request ||= controller.request
123
+ end
124
+
125
+ attr_reader :content, :view_context
126
+
127
+ # The controller used for testing components.
128
+ # Defaults to ApplicationController. This should be set early
129
+ # in the initialization process and should be set to a string.
130
+ mattr_accessor :test_controller
131
+ @@test_controller = "ApplicationController"
132
+
133
+ class << self
134
+ def inherited(child)
135
+ if defined?(Rails)
136
+ child.include Rails.application.routes.url_helpers unless child < Rails.application.routes.url_helpers
137
+ end
138
+
139
+ super
140
+ end
141
+
142
+ def call_method_name(variant)
143
+ if variant.present? && variants.include?(variant)
144
+ "call_#{variant}"
145
+ else
146
+ "call"
147
+ end
148
+ end
149
+
150
+ def source_location
151
+ @source_location ||=
152
+ begin
153
+ # Require `#initialize` to be defined so that we can use `method#source_location`
154
+ # to look up the filename of the component.
155
+ initialize_method = instance_method(:initialize)
156
+ initialize_method.source_location[0] if initialize_method.owner == self
157
+ end
158
+ end
159
+
160
+ def compiled?
161
+ @compiled && ActionView::Base.cache_template_loading
162
+ end
163
+
164
+ def inlined?
165
+ instance_methods(false).grep(/^call/).present? && templates.empty?
166
+ end
167
+
168
+ def compile!
169
+ compile(raise_template_errors: true)
170
+ end
171
+
172
+ # Compile templates to instance methods, assuming they haven't been compiled already.
173
+ # We could in theory do this on app boot, at least in production environments.
174
+ # Right now this just compiles the first time the component is rendered.
175
+ def compile(raise_template_errors: false)
176
+ return if compiled? || inlined?
177
+
178
+ if template_errors.present?
179
+ raise ViewComponent::TemplateError.new(template_errors) if raise_template_errors
180
+ return false
181
+ end
182
+
183
+ templates.each do |template|
184
+ class_eval <<-RUBY, template[:path], -1
185
+ def #{call_method_name(template[:variant])}
186
+ @output_buffer = ActionView::OutputBuffer.new
187
+ #{compiled_template(template[:path])}
188
+ end
189
+ RUBY
190
+ end
191
+
192
+ @compiled = true
193
+ end
194
+
195
+ def variants
196
+ templates.map { |template| template[:variant] }
197
+ end
198
+
199
+ # we'll eventually want to update this to support other types
200
+ def type
201
+ "text/html"
202
+ end
203
+
204
+ def identifier
205
+ source_location
206
+ end
207
+
208
+ def with_content_areas(*areas)
209
+ if areas.include?(:content)
210
+ raise ArgumentError.new ":content is a reserved content area name. Please use another name, such as ':body'"
211
+ end
212
+ attr_reader *areas
213
+ self.content_areas = areas
214
+ end
215
+
216
+ private
217
+
218
+ def matching_views_in_source_location
219
+ return [] unless source_location
220
+ (Dir["#{source_location.chomp(File.extname(source_location))}.*{#{ActionView::Template.template_handler_extensions.join(',')}}"] - [source_location])
221
+ end
222
+
223
+ def templates
224
+ @templates ||=
225
+ matching_views_in_source_location.each_with_object([]) do |path, memo|
226
+ pieces = File.basename(path).split(".")
227
+
228
+ memo << {
229
+ path: path,
230
+ variant: pieces.second.split("+").second&.to_sym,
231
+ handler: pieces.last
232
+ }
233
+ end
234
+ end
235
+
236
+ def template_errors
237
+ @template_errors ||=
238
+ begin
239
+ errors = []
240
+ if source_location.nil?
241
+ # Require `#initialize` to be defined so that we can use `method#source_location`
242
+ # to look up the filename of the component.
243
+ errors << "#{self} must implement #initialize."
244
+ end
245
+
246
+ errors << "Could not find a template file for #{self}." if templates.empty?
247
+
248
+ if templates.count { |template| template[:variant].nil? } > 1
249
+ errors << "More than one template found for #{self}. There can only be one default template file per component."
250
+ end
251
+
252
+ invalid_variants = templates
253
+ .group_by { |template| template[:variant] }
254
+ .map { |variant, grouped| variant if grouped.length > 1 }
255
+ .compact
256
+ .sort
257
+
258
+ unless invalid_variants.empty?
259
+ errors << "More than one template found for #{'variant'.pluralize(invalid_variants.count)} #{invalid_variants.map { |v| "'#{v}'" }.to_sentence} in #{self}. There can only be one template file per variant."
260
+ end
261
+ errors
262
+ end
263
+ end
264
+
265
+ def compiled_template(file_path)
266
+ handler = ActionView::Template.handler_for_extension(File.extname(file_path).gsub(".", ""))
267
+ template = File.read(file_path)
268
+
269
+ if handler.method(:call).parameters.length > 1
270
+ handler.call(self, template)
271
+ else # remove before upstreaming into Rails
272
+ handler.call(OpenStruct.new(source: template, identifier: identifier, type: type))
273
+ end
274
+ end
275
+ end
276
+
277
+ ActiveSupport.run_load_hooks(:action_view_component, self)
278
+ end
279
+ end