lib/action_view/component/base.rb in actionview-component-1.6.2 vs lib/action_view/component/base.rb in actionview-component-1.7.0
- old
+ new
@@ -9,10 +9,13 @@
include ActiveSupport::Configurable
include ActionView::Component::Previewable
delegate :form_authenticity_token, :protect_against_forgery?, to: :helpers
+ class_attribute :content_areas, default: []
+ self.content_areas = [] # default doesn't work until Rails 5.2
+
# 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
@@ -35,21 +38,22 @@
# <%= render MyComponent, title: "greeting" do %>world<% end %>
# returns:
# <span title="greeting">Hello, world!</span>
#
def render_in(view_context, *args, &block)
- self.class.compile
+ self.class.compile!
@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?
+ @content = view_context.capture(self, &block) if block_given?
+
validate!
send(self.class.call_method_name(@variant))
ensure
@current_template = old_current_template
@@ -85,10 +89,23 @@
def format # :nodoc:
@variant
end
+ def with(area, content = nil, &block)
+ unless content_areas.include?(area)
+ raise ArgumentError.new "Unknown content_area '#{area}' - expected one of '#{content_areas}'"
+ end
+
+ if block_given?
+ content = view_context.capture(&block)
+ end
+
+ instance_variable_set("@#{area}".to_sym, content)
+ nil
+ end
+
private
def request
@request ||= controller.request
end
@@ -108,38 +125,44 @@
else
"call"
end
end
- def has_initializer?
- self.instance_method(:initialize).owner == self
- 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 has_initializer?
-
- instance_method(:initialize).source_location[0]
+ @source_location ||=
+ begin
+ # 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.
+ #
+ initialize_method = instance_method(:initialize)
+ initialize_method.source_location[0] if initialize_method.owner == self
+ end
end
def compiled?
@compiled && ActionView::Base.cache_template_loading
end
+ def compile!
+ compile(validate: true)
+ end
+
# 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 first time the component is rendered.
- def compile
+ def compile(validate: false)
return if compiled?
- validate_templates
+ if template_errors.present?
+ raise ActionView::Component::TemplateError.new(template_errors) if validate
+ return false
+ end
templates.each do |template|
class_eval <<-RUBY, __FILE__, __LINE__ + 1
def #{call_method_name(template[:variant])}
@output_buffer = ActionView::OutputBuffer.new
@@ -162,42 +185,59 @@
def identifier
source_location
end
+ def with_content_areas(*areas)
+ if areas.include?(:content)
+ raise ArgumentError.new ":content is a reserved content area name. Please use another name, such as ':body'"
+ end
+ attr_reader *areas
+ self.content_areas = areas
+ end
+
private
def matching_views_in_source_location
+ return [] unless source_location
(Dir["#{source_location.sub(/#{File.extname(source_location)}$/, '')}.*{#{ActionView::Template.template_handler_extensions.join(',')}}"] - [source_location])
end
def templates
@templates ||=
matching_views_in_source_location.each_with_object([]) do |path, memo|
pieces = File.basename(path).split(".")
memo << {
path: path,
- variant: pieces.second.split("+")[1]&.to_sym,
+ variant: pieces.second.split("+").second&.to_sym,
handler: pieces.last
}
end
end
- def validate_templates
- if templates.empty?
- raise NotImplementedError.new("Could not find a template file for #{self}.")
- end
+ def template_errors
+ @template_errors ||=
+ begin
+ errors = []
+ errors << "#{self} must implement #initialize." if source_location.nil?
+ errors << "Could not find a template file for #{self}." if templates.empty?
- 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
+ if templates.count { |template| template[:variant].nil? } > 1
+ errors << "More than one template found for #{self}. There can only be one default template file per component."
+ end
- variants.each_with_object(Hash.new(0)) { |variant, counts| counts[variant] += 1 }.each do |variant, count|
- next unless count > 1
+ invalid_variants = templates
+ .group_by { |template| template[:variant] }
+ .map { |variant, grouped| variant if grouped.length > 1 }
+ .compact
+ .sort
- raise StandardError.new("More than one template found for variant '#{variant}' in #{self}. There can only be one template file per variant.")
- end
+ unless invalid_variants.empty?
+ 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."
+ end
+ errors
+ end
end
def compiled_template(file_path)
handler = ActionView::Template.handler_for_extension(File.extname(file_path).gsub(".", ""))
template = File.read(file_path)