class ViewComponent::Base

Constants

RESERVED_PARAMETER
ViewContextCalledBeforeRenderError

Attributes

source_location[RW]

@private

virtual_path[RW]

@private

view_context[R]

Public Class Methods

_after_compile() click to toggle source

EXPERIMENTAL: This API is experimental and may be removed at any time. Hook for allowing components to do work as part of the compilation process.

For example, one might compile component-specific assets at this point. @private TODO: add documentation

# File lib/view_component/base.rb, line 36
def self._after_compile
  # noop
end
new(*) click to toggle source

@private

# File lib/view_component/base.rb, line 141
def initialize(*); end

Private Class Methods

_sidecar_files(extensions) click to toggle source

EXPERIMENTAL: This API is experimental and may be removed at any time. Find sidecar files for the given extensions.

The provided array of extensions is expected to contain strings starting without the “dot”, example: `[“erb”, “haml”]`.

For example, one might collect sidecar CSS files that need to be compiled. @private TODO: add documentation

# File lib/view_component/base.rb, line 313
def _sidecar_files(extensions)
  return [] unless source_location

  extensions = extensions.join(",")

  # view files in a directory named like the component
  directory = File.dirname(source_location)
  filename = File.basename(source_location, ".rb")
  component_name = name.demodulize.underscore

  # Add support for nested components defined in the same file.
  #
  # e.g.
  #
  # class MyComponent < ViewComponent::Base
  #   class MyOtherComponent < ViewComponent::Base
  #   end
  # end
  #
  # Without this, `MyOtherComponent` will not look for `my_component/my_other_component.html.erb`
  nested_component_files =
    if name.include?("::") && component_name != filename
      Dir["#{directory}/#{filename}/#{component_name}.*{#{extensions}}"]
    else
      []
    end

  # view files in the same directory as the component
  sidecar_files = Dir["#{directory}/#{component_name}.*{#{extensions}}"]

  sidecar_directory_files = Dir["#{directory}/#{component_name}/#{filename}.*{#{extensions}}"]

  (sidecar_files - [source_location] + sidecar_directory_files + nested_component_files).uniq
end
collection_counter_parameter() click to toggle source

@private

# File lib/view_component/base.rb, line 489
def collection_counter_parameter
  "#{collection_parameter}_counter".to_sym
end
collection_iteration_parameter() click to toggle source

@private

# File lib/view_component/base.rb, line 499
def collection_iteration_parameter
  "#{collection_parameter}_iteration".to_sym
end
collection_parameter() click to toggle source

@private

# File lib/view_component/base.rb, line 480
def collection_parameter
  if provided_collection_parameter
    provided_collection_parameter
  else
    name && name.demodulize.underscore.chomp("_component").to_sym
  end
end
compile(raise_errors: false) click to toggle source

Compile templates to instance methods, assuming they haven't been compiled already.

Do as much work as possible in this step, as doing so reduces the amount of work done each time a component is rendered. @private

# File lib/view_component/base.rb, line 400
def compile(raise_errors: false)
  compiler.compile(raise_errors: raise_errors)
end
compiled?() click to toggle source

@private

# File lib/view_component/base.rb, line 391
def compiled?
  compiler.compiled?
end
compiler() click to toggle source

@private

# File lib/view_component/base.rb, line 405
def compiler
  @__vc_compiler ||= Compiler.new(self)
end
counter_argument_present?() click to toggle source

@private

# File lib/view_component/base.rb, line 494
def counter_argument_present?
  initialize_parameter_names.include?(collection_counter_parameter)
end
format() click to toggle source

@private

# File lib/view_component/base.rb, line 416
def format
  :html
end
identifier() click to toggle source

@private

# File lib/view_component/base.rb, line 421
def identifier
  source_location
end
inherited(child) click to toggle source

@private

Calls superclass method ViewComponent::SlotableV2#inherited
# File lib/view_component/base.rb, line 366
def inherited(child)
  # Compile so child will inherit compiled `call_*` template methods that
  # `compile` defines
  compile

  # If Rails application is loaded, add application url_helpers to the component context
  # we need to check this to use this gem as a dependency
  if defined?(Rails) && Rails.application
    child.include Rails.application.routes.url_helpers unless child < Rails.application.routes.url_helpers
  end

  # Derive the source location of the component Ruby file from the call stack.
  # We need to ignore `inherited` frames here as they indicate that `inherited`
  # has been re-defined by the consuming application, likely in ApplicationComponent.
  child.source_location = caller_locations(1, 10).reject { |l| l.label == "inherited" }[0].absolute_path

  # Removes the first part of the path and the extension.
  child.virtual_path = child.source_location.gsub(
    %r{(.*#{Regexp.quote(ViewComponent::Base.view_component_path)})|(\.rb)}, ""
  )

  super
end
initialize_parameter_names() click to toggle source
# File lib/view_component/base.rb, line 510
def initialize_parameter_names
  initialize_parameters.map(&:last)
end
initialize_parameters() click to toggle source
# File lib/view_component/base.rb, line 514
def initialize_parameters
  instance_method(:initialize).parameters
end
iteration_argument_present?() click to toggle source

@private

# File lib/view_component/base.rb, line 504
def iteration_argument_present?
  initialize_parameter_names.include?(collection_iteration_parameter)
end
provided_collection_parameter() click to toggle source
# File lib/view_component/base.rb, line 518
def provided_collection_parameter
  @provided_collection_parameter ||= nil
end
short_identifier() click to toggle source

Provide identifier for ActionView template annotations

@private

# File lib/view_component/base.rb, line 361
def short_identifier
  @short_identifier ||= defined?(Rails.root) ? source_location.sub("#{Rails.root}/", "") : source_location
end
type() click to toggle source

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

# File lib/view_component/base.rb, line 411
def type
  "text/html"
end
validate_collection_parameter!(validate_default: false) click to toggle source

Ensure the component initializer accepts the collection parameter. By default, we do not validate that the default parameter name is accepted, as support for collection rendering is optional. @private TODO: add documentation

# File lib/view_component/base.rb, line 440
def validate_collection_parameter!(validate_default: false)
  parameter = validate_default ? collection_parameter : provided_collection_parameter

  return unless parameter
  return if initialize_parameter_names.include?(parameter)

  # If Ruby cannot parse the component class, then the initalize
  # parameters will be empty and ViewComponent will not be able to render
  # the component.
  if initialize_parameters.empty?
    raise ArgumentError.new(
      "The #{self} initializer is empty or invalid." \
      "It must accept the parameter `#{parameter}` to render it as a collection.\n\n" \
      "To fix this issue, update the initializer to accept `#{parameter}`.\n\n" \
      "See https://viewcomponent.org/guide/collections.html for more information on rendering collections."
    )
  end

  raise ArgumentError.new(
    "The initializer for #{self} does not accept the parameter `#{parameter}`, " \
    "which is required in order to render it as a collection.\n\n" \
    "To fix this issue, update the initializer to accept `#{parameter}`.\n\n" \
    "See https://viewcomponent.org/guide/collections.html for more information on rendering collections."
  )
end
validate_initialization_parameters!() click to toggle source

Ensure the component initializer does not define invalid parameters that could override the framework's methods. @private TODO: add documentation

# File lib/view_component/base.rb, line 470
def validate_initialization_parameters!
  return unless initialize_parameter_names.include?(RESERVED_PARAMETER)

  raise ViewComponent::ComponentError.new(
    "#{self} initializer cannot accept the parameter `#{RESERVED_PARAMETER}`, as it will override a " \
    "public ViewComponent method. To fix this issue, rename the parameter."
  )
end
with_collection(collection, **args) click to toggle source

Render a component for each element in a collection ([documentation](/guide/collections)):

render(ProductsComponent.with_collection(@products, foo: :bar))

@param collection [Enumerable] A list of items to pass the ViewComponent one at a time. @param args [Arguments] Arguments to pass to the ViewComponent every time.

# File lib/view_component/base.rb, line 354
def with_collection(collection, **args)
  Collection.new(self, collection, **args)
end
with_collection_parameter(parameter) click to toggle source

Set the parameter name used when rendering elements of a collection ([documentation](/guide/collections)):

with_collection_parameter :item

@param parameter [Symbol] The parameter name used when rendering elements of a collection.

# File lib/view_component/base.rb, line 430
def with_collection_parameter(parameter)
  @provided_collection_parameter = parameter
end

Public Instance Methods

_output_postamble() click to toggle source

EXPERIMENTAL: Optional content to be returned after the rendered template.

@return [String]

# File lib/view_component/base.rb, line 113
def _output_postamble
  ""
end
before_render() click to toggle source

Called before rendering the component. Override to perform operations that depend on having access to the view context, such as helpers.

@return [void]

# File lib/view_component/base.rb, line 121
def before_render
  before_render_check
end
before_render_check() click to toggle source

Called after rendering the component.

@deprecated Use `#before_render` instead. Will be removed in v3.0.0. @return [void]

# File lib/view_component/base.rb, line 129
def before_render_check
  # noop
end
controller() click to toggle source

The current controller. Use sparingly as doing so introduces coupling that inhibits encapsulation & reuse, often making testing difficult.

@return [ActionController::Base]

# File lib/view_component/base.rb, line 162
def controller
  if view_context.nil?
    raise(
      ViewContextCalledBeforeRenderError,
      "`#controller` cannot be used during initialization, as it depends " \
      "on the view context that only exists once a ViewComponent is passed to " \
      "the Rails render pipeline.\n\n" \
      "It's sometimes possible to fix this issue by moving code dependent on " \
      "`#controller` to a `#before_render` method: https://viewcomponent.org/api.html#before_render--void."
    )
  end

  @__vc_controller ||= view_context.controller
end
format() click to toggle source

For caching, such as cache_if

@private

# File lib/view_component/base.rb, line 212
def format
  # Ruby 2.6 throws a warning without checking `defined?`, 2.7 does not
  if defined?(@__vc_variant)
    @__vc_variant
  end
end
helpers() click to toggle source

A proxy through which to access helpers. Use sparingly as doing so introduces coupling that inhibits encapsulation & reuse, often making testing difficult.

@return [ActionView::Base]

# File lib/view_component/base.rb, line 181
def helpers
  if view_context.nil?
    raise(
      ViewContextCalledBeforeRenderError,
      "`#helpers` cannot be used during initialization, as it depends " \
      "on the view context that only exists once a ViewComponent is passed to " \
      "the Rails render pipeline.\n\n" \
      "It's sometimes possible to fix this issue by moving code dependent on " \
      "`#helpers` to a `#before_render` method: https://viewcomponent.org/api.html#before_render--void."
    )
  end

  @__vc_helpers ||= controller.view_context
end
render(options = {}, args = {}, &block) click to toggle source

Re-use original view_context if we're not rendering a component.

This prevents an exception when rendering a partial inside of a component that has also been rendered outside of the component. This is due to the partials compiled template method existing in the parent `view_context`,

and not the component's `view_context`.

@private

Calls superclass method
# File lib/view_component/base.rb, line 150
def render(options = {}, args = {}, &block)
  if options.is_a? ViewComponent::Base
    super
  else
    view_context.render(options, args, &block)
  end
end
render?() click to toggle source

Override to determine whether the ViewComponent should render.

@return [Boolean]

# File lib/view_component/base.rb, line 136
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>

@private

# File lib/view_component/base.rb, line 65
def render_in(view_context, &block)
  self.class.compile(raise_errors: true)

  @view_context = view_context
  @lookup_context ||= view_context.lookup_context

  # required for path helpers in older Rails versions
  @view_renderer ||= view_context.view_renderer

  # For content_for
  @view_flow ||= view_context.view_flow

  # For i18n
  @virtual_path ||= virtual_path

  # For template variants (+phone, +desktop, etc.)
  @__vc_variant ||= @lookup_context.variants.first

  # For caching, such as #cache_if
  @current_template = nil unless defined?(@current_template)
  old_current_template = @current_template
  @current_template = self

  if block && defined?(@__vc_content_set_by_with_content)
    raise ArgumentError.new(
      "It looks like a block was provided after calling `with_content` on #{self.class.name}, " \
      "which means that ViewComponent doesn't know which content to use.\n\n" \
      "To fix this issue, use either `with_content` or a block."
    )
  end

  @__vc_content_evaluated = false
  @__vc_render_in_block = block

  before_render

  if render?
    render_template_for(@__vc_variant).to_s + _output_postamble
  else
    ""
  end
ensure
  @current_template = old_current_template
end
request() click to toggle source

The current request. Use sparingly as doing so introduces coupling that inhibits encapsulation & reuse, often making testing difficult.

@return [ActionDispatch::Request]

# File lib/view_component/base.rb, line 238
def request
  @request ||= controller.request if controller.respond_to?(:request)
end
view_cache_dependencies() click to toggle source

For caching, such as cache_if @private

# File lib/view_component/base.rb, line 205
def view_cache_dependencies
  []
end
virtual_path() click to toggle source

Exposes .virtual_path as an instance method

@private

# File lib/view_component/base.rb, line 199
def virtual_path
  self.class.virtual_path
end
with_variant(variant) click to toggle source

Use the provided variant instead of the one determined by the current request.

@deprecated Will be removed in v3.0.0. @param variant [Symbol] The variant to be used by the component. @return [self]

# File lib/view_component/base.rb, line 224
def with_variant(variant)
  ActiveSupport::Deprecation.warn(
    "`with_variant` is deprecated and will be removed in ViewComponent v3.0.0."
  )

  @__vc_variant = variant

  self
end

Private Instance Methods

content() click to toggle source
# File lib/view_component/base.rb, line 246
def content
  @__vc_content_evaluated = true
  return @__vc_content if defined?(@__vc_content)

  @__vc_content =
    if @view_context && @__vc_render_in_block
      view_context.capture(self, &@__vc_render_in_block)
    elsif defined?(@__vc_content_set_by_with_content)
      @__vc_content_set_by_with_content
    end
end
content_evaluated?() click to toggle source
# File lib/view_component/base.rb, line 258
def content_evaluated?
  @__vc_content_evaluated
end