lib/action_view/component/base.rb in actionview-component-1.13.0 vs lib/action_view/component/base.rb in actionview-component-1.14.0
- old
+ new
@@ -1,274 +1,13 @@
# frozen_string_literal: true
-require "active_support/configurable"
-
module ActionView
module Component
- class Base < ActionView::Base
+ class Base < ViewComponent::Base
include ActiveModel::Validations
- 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.
- #
- # view_context: ActionView context from calling view
- # block: optional block to be captured within the view context
- #
- # returns HTML that has been escaped by the respective template handler
- #
- # Example subclass:
- #
- # app/components/my_component.rb:
- # class MyComponent < ActionView::Component::Base
- # def initialize(title:)
- # @title = title
- # end
- # end
- #
- # app/components/my_component.html.erb
- # <span title="<%= @title %>">Hello, <%= content %>!</span>
- #
- # In use:
- # <%= render MyComponent.new(title: "greeting") do %>world<% end %>
- # returns:
- # <span title="greeting">Hello, world!</span>
- #
- def render_in(view_context, &block)
- 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(self, &block) if block_given?
-
- before_render_check
-
- if render?
- send(self.class.call_method_name(@variant))
- else
- ""
- end
- ensure
- @current_template = old_current_template
- end
-
def before_render_check
validate!
end
-
- def render?
- true
- end
-
- def initialize(*); end
-
- def render(options = {}, args = {}, &block)
- if options.is_a?(String) || (options.is_a?(Hash) && options.has_key?(:partial))
- view_context.render(options, args, &block)
- else
- super
- end
- end
-
- def controller
- @controller ||= view_context.controller
- end
-
- # Provides a proxy to access helper methods through
- def helpers
- @helpers ||= view_context
- end
-
- # Removes the first part of the path and the extension.
- def virtual_path
- self.class.source_location.gsub(%r{(.*app/components)|(\.rb)}, "")
- end
-
- def view_cache_dependencies
- []
- end
-
- 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
-
- attr_reader :content, :view_context
-
- # The controller used for testing components.
- # Defaults to ApplicationController. This should be set early
- # in the initialization process and should be set to a string.
- mattr_accessor :test_controller
- @@test_controller = "ApplicationController"
-
- 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? && variants.include?(variant)
- "call_#{variant}"
- else
- "call"
- end
- end
-
- def source_location
- @source_location ||=
- begin
- # Require `#initialize` to be defined so that we can use `method#source_location`
- # to look up the filename of the component.
- 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(validate: false)
- return if compiled?
-
- if template_errors.present?
- raise ActionView::Component::TemplateError.new(template_errors) if validate
- return false
- end
-
- templates.each do |template|
- class_eval <<-RUBY, template[:path], -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
-
- # we'll eventually want to update this to support other types
- def type
- "text/html"
- end
-
- 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.chomp(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("+").second&.to_sym,
- handler: pieces.last
- }
- end
- end
-
- def template_errors
- @template_errors ||=
- begin
- errors = []
- if source_location.nil?
- # Require `#initialize` to be defined so that we can use `method#source_location`
- # to look up the filename of the component.
- errors << "#{self} must implement #initialize."
- end
-
- errors << "Could not find a template file for #{self}." if templates.empty?
-
- 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
-
- invalid_variants = templates
- .group_by { |template| template[:variant] }
- .map { |variant, grouped| variant if grouped.length > 1 }
- .compact
- .sort
-
- 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)
-
- if handler.method(:call).parameters.length > 1
- handler.call(self, template)
- else # remove before upstreaming into Rails
- handler.call(OpenStruct.new(source: template, identifier: identifier, type: type))
- end
- end
- end
-
- ActiveSupport.run_load_hooks(:action_view_component, self)
end
end
end