actionview-component 1.7.0 → 1.17.0

Sign up to get free protection for your applications and to get access to all the features.
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