class ViewComponent::Base

Attributes

content[R]
view_context[R]

Public Class Methods

new(*) click to toggle source
# File lib/view_component/base.rb, line 74
def initialize(*); end

Private Class Methods

call_method_name(variant) click to toggle source
# 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(raise_template_errors: false) click to toggle source

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
compile!() click to toggle source
# File lib/view_component/base.rb, line 168
def compile!
  compile(raise_template_errors: true)
end
compiled?() click to toggle source
# File lib/view_component/base.rb, line 160
def compiled?
  @compiled && ActionView::Base.cache_template_loading
end
compiled_template(file_path) click to toggle source
# 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
identifier() click to toggle source
# File lib/view_component/base.rb, line 204
def identifier
  source_location
end
inherited(child) click to toggle source
Calls superclass method
# 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
inlined?() click to toggle source
# File lib/view_component/base.rb, line 164
def inlined?
  instance_methods(false).grep(/^call/).present? && templates.empty?
end
matching_views_in_source_location() click to toggle source
# 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
source_location() click to toggle source
# 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
template_errors() click to toggle source
# 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
templates() click to toggle source
# 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
type() click to toggle source

we'll eventually want to update this to support other types

# File lib/view_component/base.rb, line 200
def type
  "text/html"
end
variants() click to toggle source
# File lib/view_component/base.rb, line 195
def variants
  templates.map { |template| template[:variant] }
end
with_content_areas(*areas) click to toggle source
# 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

before_render_check() click to toggle source
# File lib/view_component/base.rb, line 66
def before_render_check
  # noop
end
controller() click to toggle source
# File lib/view_component/base.rb, line 84
def controller
  @controller ||= view_context.controller
end
helpers() click to toggle source

Provides a proxy to access helper methods through

# File lib/view_component/base.rb, line 89
def helpers
  @helpers ||= view_context
end
render(options = {}, args = {}, &block) click to toggle source
Calls superclass method
# 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
render?() click to toggle source
# File lib/view_component/base.rb, line 70
def render?
  true
end
render_in(view_context, &block) click to toggle source

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
view_cache_dependencies() click to toggle source
# File lib/view_component/base.rb, line 98
def view_cache_dependencies
  []
end
virtual_path() click to toggle source

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
with(area, content = nil, &block) click to toggle source
# 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

request() click to toggle source
# File lib/view_component/base.rb, line 121
def request
  @request ||= controller.request
end