class ViewComponent::Base
Attributes
Public Class Methods
# File lib/view_component/base.rb, line 74 def initialize(*); end
Private Class Methods
# File lib/view_component/base.rb, line 142 def call_method_name(variant) if variant.present? && variants.include?(variant) "call_#{variant}" else "call" end 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.
# File lib/view_component/base.rb, line 175 def compile(raise_template_errors: false) return if compiled? || inlined? if template_errors.present? raise ViewComponent::TemplateError.new(template_errors) if raise_template_errors 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
# File lib/view_component/base.rb, line 168 def compile! compile(raise_template_errors: true) end
# File lib/view_component/base.rb, line 160 def compiled? @compiled && ActionView::Base.cache_template_loading end
# File lib/view_component/base.rb, line 265 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
# File lib/view_component/base.rb, line 204 def identifier source_location end
# File lib/view_component/base.rb, line 134 def inherited(child) if defined?(Rails) child.include Rails.application.routes.url_helpers unless child < Rails.application.routes.url_helpers end super end
# File lib/view_component/base.rb, line 164 def inlined? instance_methods(false).grep(/^call/).present? && templates.empty? end
# File lib/view_component/base.rb, line 218 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
# File lib/view_component/base.rb, line 150 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
# File lib/view_component/base.rb, line 236 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
# File lib/view_component/base.rb, line 223 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
we'll eventually want to update this to support other types
# File lib/view_component/base.rb, line 200 def type "text/html" end
# File lib/view_component/base.rb, line 195 def variants templates.map { |template| template[:variant] } end
# File lib/view_component/base.rb, line 208 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
Public Instance Methods
# File lib/view_component/base.rb, line 66 def before_render_check # noop end
# File lib/view_component/base.rb, line 84 def controller @controller ||= view_context.controller end
Provides a proxy to access helper methods through
# File lib/view_component/base.rb, line 89 def helpers @helpers ||= view_context end
# File lib/view_component/base.rb, line 76 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
# File lib/view_component/base.rb, line 70 def render? true end
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 < ViewComponent::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>
# File lib/view_component/base.rb, line 41 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
# File lib/view_component/base.rb, line 98 def view_cache_dependencies [] end
Removes the first part of the path and the extension.
# File lib/view_component/base.rb, line 94 def virtual_path self.class.source_location.gsub(%r{(.*app/components)|(\.rb)}, "") end
# File lib/view_component/base.rb, line 106 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 Instance Methods
# File lib/view_component/base.rb, line 121 def request @request ||= controller.request end