class ViewComponent::Base
def before_render
def before_render before_render_check end
def before_render_check
def before_render_check # noop end
def compile(raise_errors: false)
Do as much work as possible in this step, as doing so reduces the amount
Compile templates to instance methods, assuming they haven't been compiled already.
def compile(raise_errors: false) template_compiler.compile(raise_errors: raise_errors) end
def compiled?
def compiled? template_compiler.compiled? end
def controller
def controller raise ViewContextCalledBeforeRenderError, "`controller` can only be called at render time." if view_context.nil? @controller ||= view_context.controller end
def format
def format # Ruby 2.6 throws a warning without checking `defined?`, 2.7 does not if defined?(@variant) @variant end end
def format
def format :html end
def helpers
def helpers raise ViewContextCalledBeforeRenderError, "`helpers` can only be called at render time." if view_context.nil? @helpers ||= controller.view_context end
def identifier
def identifier source_location end
def inherited(child)
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{(.*app/components)|(\.rb)}, "") super end
def initialize(*); end
def initialize(*); end
def initialize_parameters
def initialize_parameters instance_method(:initialize).parameters end
def provided_collection_parameter
def provided_collection_parameter @provided_collection_parameter ||= nil end
def render(options = {}, args = {}, &block)
of the component. This is due to the partials compiled template method existing in the parent `view_context`,
This prevents an exception when rendering a partial inside of a component that has also been rendered outside
Re-use original view_context if we're not rendering a component.
def render(options = {}, args = {}, &block) if options.is_a? ViewComponent::Base super else view_context.render(options, args, &block) end end
def render?
def render? true end
def render_in(view_context, &block)
Hello, world!
returns:
<%= render MyComponent.new(title: "greeting") do %>world<% end %>
In use:
Hello, <%= content %>!
app/components/my_component.html.erb
end
end
@title = title
def initialize(title:)
class MyComponent < ViewComponent::Base
app/components/my_component.rb:
Example subclass:
returns HTML that has been escaped by the respective template handler
block: optional block to be captured within the view context
view_context: ActionView context from calling view
Entrypoint for rendering components.
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.) @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 # Assign captured content passed to component as a block to @content @content = view_context.capture(self, &block) if block_given? before_render if render? render_template_for(@variant) else "" end ensure @current_template = old_current_template end
def request
Use sparingly as doing so introduces coupling
Exposes the current request to the component.
def request @request ||= controller.request end
def short_identifier
def short_identifier @short_identifier ||= defined?(Rails.root) ? source_location.sub("#{Rails.root}/", "") : source_location end
def template_compiler
def template_compiler @_template_compiler ||= Compiler.new(self) end
def type
def type "text/html" end
def validate_collection_parameter!(validate_default: false)
is accepted, as support for collection
validate that the default parameter name
collection parameter. By default, we do not
Ensure the component initializer accepts the
def validate_collection_parameter!(validate_default: false) parameter = validate_default ? collection_parameter : provided_collection_parameter return unless parameter return if initialize_parameters.map(&:last).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( "#{self} initializer is empty or invalid." ) end raise ArgumentError.new( "#{self} initializer must accept " \ "`#{parameter}` collection parameter." ) end
def view_cache_dependencies
def view_cache_dependencies [] end
def virtual_path
def virtual_path self.class.virtual_path end
def with(area, content = nil, &block)
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
def with_collection(collection, **args)
def with_collection(collection, **args) Collection.new(self, collection, **args) end
def with_collection_parameter(param)
def with_collection_parameter(param) @provided_collection_parameter = param end
def with_content_areas(*areas)
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
def with_variant(variant)
def with_variant(variant) @variant = variant self end