# frozen_string_literal: true
require "active_support/concern"
require "view_component/slot_v2"
module ViewComponent
module SlotableV2
extend ActiveSupport::Concern
# Setup component slot state
included do
# Hash of registered Slots
class_attribute :registered_slots
self.registered_slots = {}
end
class_methods do
##
# Registers a sub-component
#
# = Example
#
# renders_one :header -> (classes:) do
# HeaderComponent.new(classes: classes)
# end
#
# # OR
#
# renders_one :header, HeaderComponent
#
# where `HeaderComponent` is defined as:
#
# class HeaderComponent < ViewComponent::Base
# def initialize(classes:)
# @classes = classes
# end
# end
#
# and has the following template:
#
#
Bar
# <% end %> # <% end %> def renders_one(slot_name, callable = nil) validate_slot_name(slot_name) define_method slot_name do |*args, **kwargs, &block| if args.empty? && kwargs.empty? && block.nil? get_slot(slot_name) else set_slot(slot_name, *args, **kwargs, &block) end end register_slot(slot_name, collection: false, callable: callable) end ## # Registers a collection sub-component # # = Example # # render_many :items, -> (name:) { ItemComponent.new(name: name } # # # OR # # render_many :items, ItemComponent # # = Rendering sub-components # # The component's sidecar template can access the slot by calling a # helper method with the same name as the slot. # #One
# <% end %> # # <%= component.item(name: "Bar") do %> #two
# <% end %> # <% end %> def renders_many(slot_name, callable = nil) validate_slot_name(slot_name) singular_name = ActiveSupport::Inflector.singularize(slot_name) # Define setter for singular names # e.g. `renders_many :items` allows fetching all tabs with # `component.tabs` and setting a tab with `component.tab` define_method singular_name do |*args, **kwargs, &block| set_slot(slot_name, *args, **kwargs, &block) end # Instantiates and and adds multiple slots forwarding the first # argument to each slot constructor define_method slot_name do |collection_args = nil, &block| if collection_args.nil? && block.nil? get_slot(slot_name) else collection_args.each do |args| set_slot(slot_name, **args, &block) end end end register_slot(slot_name, collection: true, callable: callable) end # Clone slot configuration into child class # see #test_slots_pollution def inherited(child) child.registered_slots = self.registered_slots.clone super end private def register_slot(slot_name, collection:, callable:) # Setup basic slot data slot = { collection: collection, } # If callable responds to `render_in`, we set it on the slot as a renderable if callable && callable.respond_to?(:method_defined?) && callable.method_defined?(:render_in) slot[:renderable] = callable elsif callable.is_a?(String) # If callable is a string, we assume it's referencing an internal class slot[:renderable_class_name] = callable elsif callable # If slot does not respond to `render_in`, we assume it's a proc, # define a method, and save a reference to it to call when setting method_name = :"_call_#{slot_name}" define_method method_name, &callable slot[:renderable_function] = instance_method(method_name) end # Register the slot on the component self.registered_slots[slot_name] = slot end def validate_slot_name(slot_name) if slot_name.to_sym == :content raise ArgumentError.new("#{slot_name} is not a valid slot name.") end if self.registered_slots.key?(slot_name) # TODO remove? This breaks overriding slots when slots are inherited raise ArgumentError.new("#{slot_name} slot declared multiple times") end end end def get_slot(slot_name) content unless content_evaluated? # ensure content is loaded so slots will be defined slot = self.class.registered_slots[slot_name] @_set_slots ||= {} if @_set_slots[slot_name] return @_set_slots[slot_name] end if slot[:collection] [] else nil end end def set_slot(slot_name, *args, **kwargs, &block) slot_definition = self.class.registered_slots[slot_name] slot = SlotV2.new(self) # Passing the block to the sub-component wrapper like this has two # benefits: # # 1. If this is a `content_area` style sub-component, we will render the # block via the `slot` # # 2. Since we have to pass block content to components when calling # `render`, evaluating the block here would require us to call # `view_context.capture` twice, which is slower slot._content_block = block if block_given? # If class if slot_definition[:renderable] slot._component_instance = slot_definition[:renderable].new(*args, **kwargs) # If class name as a string elsif slot_definition[:renderable_class_name] slot._component_instance = self.class.const_get(slot_definition[:renderable_class_name]).new(*args, **kwargs) # If passed a lambda elsif slot_definition[:renderable_function] # Use `bind(self)` to ensure lambda is executed in the context of the # current component. This is necessary to allow the lambda to access helper # methods like `content_tag` as well as parent component state. renderable_value = if block_given? slot_definition[:renderable_function].bind(self).call(*args, **kwargs) do |*args, **kwargs| view_context.capture(*args, **kwargs, &block) end else slot_definition[:renderable_function].bind(self).call(*args, **kwargs) end # Function calls can return components, so if it's a component handle it specially if renderable_value.respond_to?(:render_in) slot._component_instance = renderable_value else slot._content = renderable_value end end @_set_slots ||= {} if slot_definition[:collection] @_set_slots[slot_name] ||= [] @_set_slots[slot_name].push(slot) else @_set_slots[slot_name] = slot end slot end end end