-
Notifications
You must be signed in to change notification settings - Fork 415
/
slotable.rb
121 lines (102 loc) · 3.97 KB
/
slotable.rb
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
# frozen_string_literal: true
require "active_support/concern"
require "view_component/slot"
module ViewComponent
module Slotable
extend ActiveSupport::Concern
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)
slot_names.each do |slot_name|
# Ensure slot_name is not already declared
if self.slots.key?(slot_name)
raise ArgumentError.new("#{slot_name} slot declared multiple times")
end
# Ensure slot name is not :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
def #{accessor_name}
#{instance_variable_name} ||= []
end
RUBY
else
attr_reader accessor_name
end
# Default class_name to ViewComponent::Slot
class_name = "ViewComponent::Slot" unless class_name.present?
# Register the slot on the component
self.slots[slot_name] = {
class_name: class_name,
instance_variable_name: instance_variable_name,
collection: collection
}
end
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` does not exist
unless slots.keys.include?(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_given?
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 should not output anything to the view itself.
nil
end
end
end