module ViewComponent::Slotable
def define_slot(slot_name, collection:, callable:)
def define_slot(slot_name, collection:, callable:) slot = {collection: collection} return slot unless callable # If callable responds to `render_in`, we set it on the slot as a renderable if 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.respond_to?(:call) # If slot doesn't 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) else raise(InvalidSlotDefinitionError) end slot end
def get_slot(slot_name)
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] @__vc_set_slots ||= {} if @__vc_set_slots[slot_name] return @__vc_set_slots[slot_name] end if slot[:collection] [] end end
def inherited(child)
def inherited(child) # Clone slot configuration into child class # see #test_slots_pollution child.registered_slots = registered_slots.clone # Add a module for slot methods, allowing them to be overriden by the component class # see #test_slot_name_can_be_overriden unless child.const_defined?(:GeneratedSlotMethods, false) generated_slot_methods = Module.new child.const_set(:GeneratedSlotMethods, generated_slot_methods) child.include generated_slot_methods end super end
def raise_if_slot_conflicts_with_call(slot_name)
def raise_if_slot_conflicts_with_call(slot_name) if slot_name.start_with?("call_") raise InvalidSlotNameError, "Slot cannot start with 'call_'. Please rename #{slot_name}" end end
def raise_if_slot_ends_with_question_mark(slot_name)
def raise_if_slot_ends_with_question_mark(slot_name) raise SlotPredicateNameError.new(name, slot_name) if slot_name.to_s.end_with?("?") end
def raise_if_slot_name_uncountable(slot_name)
def raise_if_slot_name_uncountable(slot_name) slot_name = slot_name.to_s if slot_name.pluralize == slot_name.singularize raise UncountableSlotNameError.new(name, slot_name) end end
def raise_if_slot_registered(slot_name)
def raise_if_slot_registered(slot_name) if registered_slots.key?(slot_name) # TODO remove? This breaks overriding slots when slots are inherited raise RedefinedSlotError.new(name, slot_name) end end
def register_default_slots
def register_default_slots registered_slots.each do |slot_name, config| config[:default_method] = instance_methods.find { |method_name| method_name == :"default_#{slot_name}" } registered_slots[slot_name] = config end end
def register_polymorphic_slot(slot_name, types, collection:)
def register_polymorphic_slot(slot_name, types, collection:) self::GeneratedSlotMethods.define_method(slot_name) do get_slot(slot_name) end self::GeneratedSlotMethods.define_method(:"#{slot_name}?") do get_slot(slot_name).present? end renderable_hash = types.each_with_object({}) do |(poly_type, poly_attributes_or_callable), memo| if poly_attributes_or_callable.is_a?(Hash) poly_callable = poly_attributes_or_callable[:renders] poly_slot_name = poly_attributes_or_callable[:as] else poly_callable = poly_attributes_or_callable poly_slot_name = nil end poly_slot_name ||= if collection "#{ActiveSupport::Inflector.singularize(slot_name)}_#{poly_type}" else "#{slot_name}_#{poly_type}" end memo[poly_type] = define_slot( poly_slot_name, collection: collection, callable: poly_callable ) setter_method_name = :"with_#{poly_slot_name}" if instance_methods.include?(setter_method_name) raise AlreadyDefinedPolymorphicSlotSetterError.new(setter_method_name, poly_slot_name) end define_method(setter_method_name) do |*args, &block| set_polymorphic_slot(slot_name, poly_type, *args, &block) end ruby2_keywords(setter_method_name) if respond_to?(:ruby2_keywords, true) define_method :"with_#{poly_slot_name}_content" do |content| send(setter_method_name) { content.to_s } self end end registered_slots[slot_name] = { collection: collection, renderable_hash: renderable_hash } end
def register_slot(slot_name, **kwargs)
def register_slot(slot_name, **kwargs) registered_slots[slot_name] = define_slot(slot_name, **kwargs) end
def renders_many(slot_name, callable = nil)
<% end %>
two
<% component.with_item(name: "Bar") do %>
<% end %>
One
<% component.with_item(name: "Foo") do %>
<%= render_inline(MyComponent.new) do |component| %>
method can be called multiple times to append to the slot.
helper method with the same name as the slot prefixed with `with_`. The
Consumers of the component can set the content of a slot by calling a
= Setting sub-component content
<% end %>
<%= item %>
<% items.each do |item| %>
helper method with the same name as the slot.
The component's sidecar template can access the slot by calling a
= Rendering sub-components
renders_many :items, ItemComponent
# OR
renders_many :items, -> (name:) { ItemComponent.new(name: name }
= Example
Registers a collection sub-component
#
def renders_many(slot_name, callable = nil) validate_plural_slot_name(slot_name) if callable.is_a?(Hash) && callable.key?(:types) register_polymorphic_slot(slot_name, callable[:types], collection: true) else singular_name = ActiveSupport::Inflector.singularize(slot_name) validate_singular_slot_name(ActiveSupport::Inflector.singularize(slot_name).to_sym) setter_method_name = :"with_#{singular_name}" define_method setter_method_name do |*args, &block| set_slot(slot_name, nil, *args, &block) end ruby2_keywords(setter_method_name) if respond_to?(:ruby2_keywords, true) define_method :"with_#{singular_name}_content" do |content| send(setter_method_name) { content.to_s } self end define_method :"with_#{slot_name}" do |collection_args = nil, &block| collection_args.map do |args| if args.respond_to?(:to_hash) set_slot(slot_name, nil, **args, &block) else set_slot(slot_name, nil, *args, &block) end end end self::GeneratedSlotMethods.define_method slot_name do get_slot(slot_name) end self::GeneratedSlotMethods.define_method :"#{slot_name}?" do get_slot(slot_name).present? end register_slot(slot_name, collection: true, callable: callable) end end
def renders_one(slot_name, callable = nil)
on the component instance.
Additionally, content can be set by calling `with_SLOT_NAME_content`
<% end %>
<% end %>
Bar
<% component.with_header(classes: "Foo") do %>
<%= render_inline(MyComponent.new) do |component| %>
helper method with the same name as the slot prefixed with `with_`.
Consumers of the component can render a sub-component by calling a
= Setting sub-component content
<% end %>
My header title
<%= header do %>
helper method with the same name as the sub-component.
The component's sidecar template can access the sub-component by calling a
= Rendering sub-component content
<%= content %>
and has the following template:
end
end
@classes = classes
def initialize(classes:)
class HeaderComponent < ViewComponent::Base
where `HeaderComponent` is defined as:
renders_one :header, HeaderComponent
# OR
end
HeaderComponent.new(classes: classes)
renders_one :header -> (classes:) do
= Example
Registers a sub-component
#
and has the following template:
end
end
@classes = classes
def initialize(classes:)
class HeaderComponent < ViewComponent::Base
where `HeaderComponent` is defined as:
renders_one :header, HeaderComponent
# OR
end
HeaderComponent.new(classes: classes)
renders_one :header -> (classes:) do
= Example
Registers a sub-component
#
def renders_one(slot_name, callable = nil) validate_singular_slot_name(slot_name) if callable.is_a?(Hash) && callable.key?(:types) register_polymorphic_slot(slot_name, callable[:types], collection: false) else validate_plural_slot_name(ActiveSupport::Inflector.pluralize(slot_name).to_sym) setter_method_name = :"with_#{slot_name}" define_method setter_method_name do |*args, &block| set_slot(slot_name, nil, *args, &block) end ruby2_keywords(setter_method_name) if respond_to?(:ruby2_keywords, true) self::GeneratedSlotMethods.define_method slot_name do get_slot(slot_name) end self::GeneratedSlotMethods.define_method :"#{slot_name}?" do get_slot(slot_name).present? end define_method :"with_#{slot_name}_content" do |content| send(setter_method_name) { content.to_s } self end register_slot(slot_name, collection: false, callable: callable) end end
def set_polymorphic_slot(slot_name, poly_type = nil, *args, &block)
def set_polymorphic_slot(slot_name, poly_type = nil, *args, &block) slot_definition = self.class.registered_slots[slot_name] if !slot_definition[:collection] && (defined?(@__vc_set_slots) && @__vc_set_slots[slot_name]) raise ContentAlreadySetForPolymorphicSlotError.new(slot_name) end poly_def = slot_definition[:renderable_hash][poly_type] set_slot(slot_name, poly_def, *args, &block) end
def set_slot(slot_name, slot_definition = nil, *args, &block)
def set_slot(slot_name, slot_definition = nil, *args, &block) slot_definition ||= self.class.registered_slots[slot_name] slot = Slot.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.__vc_content_block = block if block # If class if slot_definition[:renderable] slot.__vc_component_instance = slot_definition[:renderable].new(*args) # If class name as a string elsif slot_definition[:renderable_class_name] slot.__vc_component_instance = self.class.const_get(slot_definition[:renderable_class_name]).new(*args) # 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_function = slot_definition[:renderable_function].bind(self) renderable_value = if block renderable_function.call(*args) do |*rargs| view_context.capture(*rargs, &block) end else renderable_function.call(*args) end # Function calls can return components, so if it's a component handle it specially if renderable_value.respond_to?(:render_in) slot.__vc_component_instance = renderable_value else slot.__vc_content = renderable_value end end @__vc_set_slots ||= {} if slot_definition[:collection] @__vc_set_slots[slot_name] ||= [] @__vc_set_slots[slot_name].push(slot) else @__vc_set_slots[slot_name] = slot end slot end
def slot_type(slot_name)
def slot_type(slot_name) registered_slot = registered_slots[slot_name] if registered_slot registered_slot[:collection] ? :collection : :single else plural_slot_name = ActiveSupport::Inflector.pluralize(slot_name).to_sym plural_registered_slot = registered_slots[plural_slot_name] plural_registered_slot&.fetch(:collection) ? :collection_item : nil end end
def validate_plural_slot_name(slot_name)
def validate_plural_slot_name(slot_name) if RESERVED_NAMES[:plural].include?(slot_name.to_sym) raise ReservedPluralSlotNameError.new(name, slot_name) end raise_if_slot_name_uncountable(slot_name) raise_if_slot_conflicts_with_call(slot_name) raise_if_slot_ends_with_question_mark(slot_name) raise_if_slot_registered(slot_name) end
def validate_singular_slot_name(slot_name)
def validate_singular_slot_name(slot_name) if slot_name.to_sym == :content raise ContentSlotNameError.new(name) end if RESERVED_NAMES[:singular].include?(slot_name.to_sym) raise ReservedSingularSlotNameError.new(name, slot_name) end raise_if_slot_conflicts_with_call(slot_name) raise_if_slot_ends_with_question_mark(slot_name) raise_if_slot_registered(slot_name) end