File: slotable.rb

package info (click to toggle)
ruby-view-component 2.74.1-1
  • links: PTS, VCS
  • area: contrib
  • in suites: bookworm
  • size: 3,156 kB
  • sloc: ruby: 6,731; sh: 163; javascript: 10; makefile: 4
file content (146 lines) | stat: -rw-r--r-- 4,941 bytes parent folder | download
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