1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146
|
# frozen_string_literal: true
require "active_support/concern"
require "view_component/slot"
module ViewComponent
module Slotable
extend ActiveSupport::Concern
included do
# Hash of registered Slots
class_attribute :slots
self.slots = {}
end
class_methods do
# support initializing slots as:
#
# with_slot(
# :header,
# collection: true|false,
# class_name: "Header" # class name string, used to instantiate Slot
# )
def with_slot(*slot_names, collection: false, class_name: nil)
ViewComponent::Deprecation.warn(
"`with_slot` is deprecated and will be removed in ViewComponent v3.0.0.\n" \
"Use the new slots API (https://viewcomponent.org/guide/slots.html) instead."
)
slot_names.each do |slot_name|
# Ensure slot_name isn't already declared
if slots.key?(slot_name)
raise ArgumentError.new("#{slot_name} slot declared multiple times")
end
# Ensure slot name isn't :content
if slot_name == :content
raise ArgumentError.new ":content is a reserved slot name. Please use another name, such as ':body'"
end
# Set the name of the method used to access the Slot(s)
accessor_name =
if collection
# If Slot is a collection, set the accessor
# name to the pluralized form of the slot name
# For example: :tab => :tabs
ActiveSupport::Inflector.pluralize(slot_name)
else
slot_name
end
instance_variable_name = "@#{accessor_name}"
# If the slot is a collection, define an accesor that defaults to an empty array
if collection
class_eval <<-RUBY, __FILE__, __LINE__ + 1
def #{accessor_name}
content unless content_evaluated? # ensure content is loaded so slots will be defined
#{instance_variable_name} ||= []
end
RUBY
else
class_eval <<-RUBY, __FILE__, __LINE__ + 1
def #{accessor_name}
content unless content_evaluated? # ensure content is loaded so slots will be defined
#{instance_variable_name} if defined?(#{instance_variable_name})
end
RUBY
end
# Default class_name to ViewComponent::Slot
class_name = "ViewComponent::Slot" unless class_name.present?
# Register the slot on the component
slots[slot_name] = {
class_name: class_name,
instance_variable_name: instance_variable_name,
collection: collection
}
end
end
def inherited(child)
# Clone slot configuration into child class
# see #test_slots_pollution
child.slots = slots.clone
super
end
end
# Build a Slot instance on a component,
# exposing it for use inside the
# component template.
#
# slot: Name of Slot, in symbol form
# **args: Arguments to be passed to Slot initializer
#
# For example:
# <%= render(SlotsComponent.new) do |component| %>
# <% component.slot(:footer, class_names: "footer-class") do %>
# <p>This is my footer!</p>
# <% end %>
# <% end %>
#
def slot(slot_name, **args, &block)
# Raise ArgumentError if `slot` doesn't exist
unless slots.key?(slot_name)
raise ArgumentError.new "Unknown slot '#{slot_name}' - expected one of '#{slots.keys}'"
end
slot = slots[slot_name]
# The class name of the Slot, such as Header
slot_class = self.class.const_get(slot[:class_name])
unless slot_class <= ViewComponent::Slot
raise ArgumentError.new "#{slot[:class_name]} must inherit from ViewComponent::Slot"
end
# Instantiate Slot class, accommodating Slots that don't accept arguments
slot_instance = args.present? ? slot_class.new(**args) : slot_class.new
# Capture block and assign to slot_instance#content
slot_instance.content = view_context.capture(&block).to_s.strip.html_safe if block
if slot[:collection]
# Initialize instance variable as an empty array
# if slot is a collection and has yet to be initialized
unless instance_variable_defined?(slot[:instance_variable_name])
instance_variable_set(slot[:instance_variable_name], [])
end
# Append Slot instance to collection accessor Array
instance_variable_get(slot[:instance_variable_name]) << slot_instance
else
# Assign the Slot instance to the slot accessor
instance_variable_set(slot[:instance_variable_name], slot_instance)
end
# Return nil, as this method shouldn't output anything to the view itself.
nil
end
end
end
|