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