lib/action_view/component/base.rb in actionview-component-1.3.6 vs lib/action_view/component/base.rb in actionview-component-1.4.0
- old
+ new
@@ -7,11 +7,11 @@
class ActionView::Base
module RenderMonkeyPatch
def render(options = {}, args = {}, &block)
if options.respond_to?(:render_in)
ActiveSupport::Deprecation.warn(
- "passing component instances to `render` has been deprecated and will be removed in v2.0.0. Use `render MyComponent, foo: :bar` instead."
+ "passing component instances (`render MyComponent.new(foo: :bar)`) has been deprecated and will be removed in v2.0.0. Use `render MyComponent, foo: :bar` instead."
)
options.render_in(self, &block)
elsif options.is_a?(Class) && options < ActionView::Component::Base
options.new(args).render_in(self, &block)
@@ -31,10 +31,12 @@
class Base < ActionView::Base
include ActiveModel::Validations
include ActiveSupport::Configurable
include ActionController::RequestForgeryProtection
+ validate :variant_exists
+
# Entrypoint for rendering components. Called by ActionView::Base#render.
#
# view_context: ActionView context from calling view
# args(hash): params to be passed to component being rendered
# block: optional block to be captured within the view context
@@ -63,14 +65,20 @@
@view_context = view_context
@view_renderer ||= view_context.view_renderer
@lookup_context ||= view_context.lookup_context
@view_flow ||= view_context.view_flow
@virtual_path ||= virtual_path
+ @variant = @lookup_context.variants.first
+ old_current_template = @current_template
+ @current_template = self
@content = view_context.capture(&block) if block_given?
validate!
- call
+
+ send(self.class.call_method_name(@variant))
+ ensure
+ @current_template = old_current_template
end
def initialize(*); end
def render(options = {}, args = {}, &block)
@@ -86,78 +94,126 @@
end
# Looks for the source file path of the initialize method of the instance's class.
# Removes the first part of the path and the extension.
def virtual_path
- self.class.source_location.gsub(%r{(.*app/)|(.rb)}, "")
+ self.class.source_location.gsub(%r{(.*app/)|(\.rb)}, "")
end
+ def view_cache_dependencies
+ []
+ end
+
+ def format # :nodoc:
+ @variant
+ end
+
+ private
+
+ def variant_exists
+ return if self.class.variants.include?(@variant) || @variant.nil?
+
+ errors.add(:variant, "'#{@variant}' has no template defined")
+ end
+
+ def request
+ @request ||= controller.request
+ end
+
+ attr_reader :content, :view_context
+
class << self
def inherited(child)
child.include Rails.application.routes.url_helpers unless child < Rails.application.routes.url_helpers
super
end
+ def call_method_name(variant)
+ if variant.present?
+ "call_#{variant}"
+ else
+ "call"
+ end
+ end
+
def source_location
+ # Require #initialize to be defined so that we can use
+ # method#source_location to look up the file name
+ # of the component.
+ #
+ # If we were able to only support Ruby 2.7+,
+ # We could just use Module#const_source_location,
+ # rendering this unnecessary.
+ raise NotImplementedError.new("#{self} must implement #initialize.") unless self.instance_method(:initialize).owner == self
+
instance_method(:initialize).source_location[0]
end
- # Compile template to #call instance method, assuming it hasn't been compiled already.
+ # Compile templates to instance methods, assuming they haven't been compiled already.
# We could in theory do this on app boot, at least in production environments.
- # Right now this just compiles the template the first time the component is rendered.
+ # Right now this just compiles the first time the component is rendered.
def compile
return if @compiled && ActionView::Base.cache_template_loading
- ensure_initializer_defined
- class_eval <<-RUBY, __FILE__, __LINE__ + 1
- def call
- @output_buffer = ActionView::OutputBuffer.new
- #{compiled_template}
- end
- RUBY
+ validate_templates
+ templates.each do |template|
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
+ def #{call_method_name(template[:variant])}
+ @output_buffer = ActionView::OutputBuffer.new
+ #{compiled_template(template[:path])}
+ end
+ RUBY
+ end
+
@compiled = true
end
+ def variants
+ templates.map { |template| template[:variant] }
+ end
+
private
- # Require #initialize to be defined so that we can use
- # method#source_location to look up the file name
- # of the component.
- #
- # If we were able to only support Ruby 2.7+,
- # We could just use Module#const_source_location,
- # rendering this unnecessary.
- def ensure_initializer_defined
- raise NotImplementedError.new("#{self} must implement #initialize.") unless self.instance_method(:initialize).owner == self
+ def templates
+ @templates ||=
+ (Dir["#{source_location.sub(/#{File.extname(source_location)}$/, '')}.*{#{ActionView::Template.template_handler_extensions.join(',')}}"] - [source_location]).each_with_object([]) do |path, memo|
+ memo << {
+ path: path,
+ variant: path.split(".").second.split("+")[1]&.to_sym,
+ handler: path.split(".").last
+ }
+ end
end
- def compiled_template
- handler = ActionView::Template.handler_for_extension(File.extname(template_file_path).gsub(".", ""))
- template = File.read(template_file_path)
+ def validate_templates
+ if templates.empty?
+ raise NotImplementedError.new("Could not find a template file for #{self}.")
+ end
- if handler.method(:call).parameters.length > 1
- handler.call(DummyTemplate.new, template)
- else
- handler.call(DummyTemplate.new(template))
+ if templates.select { |template| template[:variant].nil? }.length > 1
+ raise StandardError.new("More than one template found for #{self}. There can only be one default template file per component.")
end
- end
- def template_file_path
- sibling_template_files =
- Dir["#{source_location.split(".")[0]}.*{#{ActionView::Template.template_handler_extensions.join(',')}}"] - [source_location]
+ variants.each_with_object(Hash.new(0)) { |variant, counts| counts[variant] += 1 }.each do |variant, count|
+ next unless count > 1
- if sibling_template_files.length > 1
- raise StandardError.new("More than one template found for #{self}. There can only be one sidecar template file per component.")
+ raise StandardError.new("More than one template found for variant '#{variant}' in #{self}. There can only be one template file per variant.")
end
+ end
- if sibling_template_files.length == 0
- raise NotImplementedError.new("Could not find a template file for #{self}.")
- end
+ def compiled_template(file_path)
+ handler = ActionView::Template.handler_for_extension(File.extname(file_path).gsub(".", ""))
+ template = File.read(file_path)
- sibling_template_files[0]
+ # This can be removed once this code is merged into Rails
+ if handler.method(:call).parameters.length > 1
+ handler.call(DummyTemplate.new, template)
+ else
+ handler.call(DummyTemplate.new(template))
+ end
end
end
class DummyTemplate
attr_reader :source
@@ -173,16 +229,8 @@
# we'll eventually want to update this to support other types
def type
"text/html"
end
end
-
- private
-
- def request
- @request ||= controller.request
- end
-
- attr_reader :content, :view_context
end
end
end