view_component 3.0.0.rc1 → 3.0.0.rc2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of view_component might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/docs/CHANGELOG.md +48 -0
- data/lib/view_component/base.rb +27 -11
- data/lib/view_component/capture_compatibility.rb +42 -0
- data/lib/view_component/config.rb +9 -1
- data/lib/view_component/engine.rb +12 -5
- data/lib/view_component/rails/tasks/view_component.rake +1 -1
- data/lib/view_component/{slot_v2.rb → slot.rb} +1 -1
- data/lib/view_component/slotable.rb +361 -45
- data/lib/view_component/system_test_helpers.rb +1 -1
- data/lib/view_component/test_helpers.rb +46 -46
- data/lib/view_component/version.rb +1 -1
- data/lib/view_component.rb +1 -0
- metadata +4 -5
- data/lib/view_component/polymorphic_slots.rb +0 -91
- data/lib/view_component/slotable_v2.rb +0 -336
| @@ -1,80 +1,396 @@ | |
| 1 1 | 
             
            # frozen_string_literal: true
         | 
| 2 2 |  | 
| 3 3 | 
             
            require "active_support/concern"
         | 
| 4 | 
            -
             | 
| 5 4 | 
             
            require "view_component/slot"
         | 
| 6 5 |  | 
| 7 6 | 
             
            module ViewComponent
         | 
| 8 7 | 
             
              module Slotable
         | 
| 9 8 | 
             
                extend ActiveSupport::Concern
         | 
| 10 9 |  | 
| 10 | 
            +
                RESERVED_NAMES = {
         | 
| 11 | 
            +
                  singular: %i[content render].freeze,
         | 
| 12 | 
            +
                  plural: %i[contents renders].freeze
         | 
| 13 | 
            +
                }.freeze
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                # Setup component slot state
         | 
| 11 16 | 
             
                included do
         | 
| 12 17 | 
             
                  # Hash of registered Slots
         | 
| 13 | 
            -
                  class_attribute : | 
| 14 | 
            -
                  self. | 
| 18 | 
            +
                  class_attribute :registered_slots
         | 
| 19 | 
            +
                  self.registered_slots = {}
         | 
| 15 20 | 
             
                end
         | 
| 16 21 |  | 
| 17 22 | 
             
                class_methods do
         | 
| 18 | 
            -
                   | 
| 19 | 
            -
             | 
| 20 | 
            -
             | 
| 21 | 
            -
             | 
| 23 | 
            +
                  ##
         | 
| 24 | 
            +
                  # Registers a sub-component
         | 
| 25 | 
            +
                  #
         | 
| 26 | 
            +
                  # = Example
         | 
| 27 | 
            +
                  #
         | 
| 28 | 
            +
                  #   renders_one :header -> (classes:) do
         | 
| 29 | 
            +
                  #     HeaderComponent.new(classes: classes)
         | 
| 30 | 
            +
                  #   end
         | 
| 31 | 
            +
                  #
         | 
| 32 | 
            +
                  #   # OR
         | 
| 33 | 
            +
                  #
         | 
| 34 | 
            +
                  #   renders_one :header, HeaderComponent
         | 
| 35 | 
            +
                  #
         | 
| 36 | 
            +
                  #   where `HeaderComponent` is defined as:
         | 
| 37 | 
            +
                  #
         | 
| 38 | 
            +
                  #   class HeaderComponent < ViewComponent::Base
         | 
| 39 | 
            +
                  #     def initialize(classes:)
         | 
| 40 | 
            +
                  #       @classes = classes
         | 
| 41 | 
            +
                  #     end
         | 
| 42 | 
            +
                  #   end
         | 
| 43 | 
            +
                  #
         | 
| 44 | 
            +
                  #   and has the following template:
         | 
| 45 | 
            +
                  #
         | 
| 46 | 
            +
                  #   <header class="<%= @classes %>">
         | 
| 47 | 
            +
                  #     <%= content %>
         | 
| 48 | 
            +
                  #   </header>
         | 
| 49 | 
            +
                  #
         | 
| 50 | 
            +
                  # = Rendering sub-component content
         | 
| 51 | 
            +
                  #
         | 
| 52 | 
            +
                  # The component's sidecar template can access the sub-component by calling a
         | 
| 53 | 
            +
                  # helper method with the same name as the sub-component.
         | 
| 54 | 
            +
                  #
         | 
| 55 | 
            +
                  #   <h1>
         | 
| 56 | 
            +
                  #     <%= header do %>
         | 
| 57 | 
            +
                  #       My header title
         | 
| 58 | 
            +
                  #     <% end %>
         | 
| 59 | 
            +
                  #   </h1>
         | 
| 60 | 
            +
                  #
         | 
| 61 | 
            +
                  # = Setting sub-component content
         | 
| 62 | 
            +
                  #
         | 
| 63 | 
            +
                  # Consumers of the component can render a sub-component by calling a
         | 
| 64 | 
            +
                  # helper method with the same name as the slot prefixed with `with_`.
         | 
| 65 | 
            +
                  #
         | 
| 66 | 
            +
                  #   <%= render_inline(MyComponent.new) do |component| %>
         | 
| 67 | 
            +
                  #     <% component.with_header(classes: "Foo") do %>
         | 
| 68 | 
            +
                  #       <p>Bar</p>
         | 
| 69 | 
            +
                  #     <% end %>
         | 
| 70 | 
            +
                  #   <% end %>
         | 
| 71 | 
            +
                  def renders_one(slot_name, callable = nil)
         | 
| 72 | 
            +
                    validate_singular_slot_name(slot_name)
         | 
| 73 | 
            +
             | 
| 74 | 
            +
                    if callable.is_a?(Hash) && callable.key?(:types)
         | 
| 75 | 
            +
                      register_polymorphic_slot(slot_name, callable[:types], collection: false)
         | 
| 76 | 
            +
                    else
         | 
| 77 | 
            +
                      validate_plural_slot_name(ActiveSupport::Inflector.pluralize(slot_name).to_sym)
         | 
| 78 | 
            +
             | 
| 79 | 
            +
                      define_method :"with_#{slot_name}" do |*args, &block|
         | 
| 80 | 
            +
                        set_slot(slot_name, nil, *args, &block)
         | 
| 81 | 
            +
                      end
         | 
| 82 | 
            +
                      ruby2_keywords(:"with_#{slot_name}") if respond_to?(:ruby2_keywords, true)
         | 
| 83 | 
            +
             | 
| 84 | 
            +
                      define_method slot_name do |*args, &block|
         | 
| 85 | 
            +
                        get_slot(slot_name)
         | 
| 86 | 
            +
                      end
         | 
| 87 | 
            +
                      ruby2_keywords(slot_name.to_sym) if respond_to?(:ruby2_keywords, true)
         | 
| 88 | 
            +
             | 
| 89 | 
            +
                      define_method "#{slot_name}?" do
         | 
| 90 | 
            +
                        get_slot(slot_name).present?
         | 
| 91 | 
            +
                      end
         | 
| 92 | 
            +
             | 
| 93 | 
            +
                      register_slot(slot_name, collection: false, callable: callable)
         | 
| 94 | 
            +
                    end
         | 
| 95 | 
            +
                  end
         | 
| 96 | 
            +
             | 
| 97 | 
            +
                  ##
         | 
| 98 | 
            +
                  # Registers a collection sub-component
         | 
| 99 | 
            +
                  #
         | 
| 100 | 
            +
                  # = Example
         | 
| 101 | 
            +
                  #
         | 
| 102 | 
            +
                  #   renders_many :items, -> (name:) { ItemComponent.new(name: name }
         | 
| 103 | 
            +
                  #
         | 
| 104 | 
            +
                  #   # OR
         | 
| 105 | 
            +
                  #
         | 
| 106 | 
            +
                  #   renders_many :items, ItemComponent
         | 
| 107 | 
            +
                  #
         | 
| 108 | 
            +
                  # = Rendering sub-components
         | 
| 109 | 
            +
                  #
         | 
| 110 | 
            +
                  # The component's sidecar template can access the slot by calling a
         | 
| 111 | 
            +
                  # helper method with the same name as the slot.
         | 
| 112 | 
            +
                  #
         | 
| 113 | 
            +
                  #   <h1>
         | 
| 114 | 
            +
                  #     <% items.each do |item| %>
         | 
| 115 | 
            +
                  #       <%= item %>
         | 
| 116 | 
            +
                  #     <% end %>
         | 
| 117 | 
            +
                  #   </h1>
         | 
| 118 | 
            +
                  #
         | 
| 119 | 
            +
                  # = Setting sub-component content
         | 
| 120 | 
            +
                  #
         | 
| 121 | 
            +
                  # Consumers of the component can set the content of a slot by calling a
         | 
| 122 | 
            +
                  # helper method with the same name as the slot prefixed with `with_`. The
         | 
| 123 | 
            +
                  # method can be called multiple times to append to the slot.
         | 
| 124 | 
            +
                  #
         | 
| 125 | 
            +
                  #   <%= render_inline(MyComponent.new) do |component| %>
         | 
| 126 | 
            +
                  #     <% component.with_item(name: "Foo") do %>
         | 
| 127 | 
            +
                  #       <p>One</p>
         | 
| 128 | 
            +
                  #     <% end %>
         | 
| 129 | 
            +
                  #
         | 
| 130 | 
            +
                  #     <% component.with_item(name: "Bar") do %>
         | 
| 131 | 
            +
                  #       <p>two</p>
         | 
| 132 | 
            +
                  #     <% end %>
         | 
| 133 | 
            +
                  #   <% end %>
         | 
| 134 | 
            +
                  def renders_many(slot_name, callable = nil)
         | 
| 135 | 
            +
                    validate_plural_slot_name(slot_name)
         | 
| 136 | 
            +
             | 
| 137 | 
            +
                    if callable.is_a?(Hash) && callable.key?(:types)
         | 
| 138 | 
            +
                      register_polymorphic_slot(slot_name, callable[:types], collection: true)
         | 
| 139 | 
            +
                    else
         | 
| 140 | 
            +
                      singular_name = ActiveSupport::Inflector.singularize(slot_name)
         | 
| 141 | 
            +
                      validate_singular_slot_name(ActiveSupport::Inflector.singularize(slot_name).to_sym)
         | 
| 142 | 
            +
             | 
| 143 | 
            +
                      define_method :"with_#{singular_name}" do |*args, &block|
         | 
| 144 | 
            +
                        set_slot(slot_name, nil, *args, &block)
         | 
| 145 | 
            +
                      end
         | 
| 146 | 
            +
                      ruby2_keywords(:"with_#{singular_name}") if respond_to?(:ruby2_keywords, true)
         | 
| 147 | 
            +
             | 
| 148 | 
            +
                      define_method :"with_#{slot_name}" do |collection_args = nil, &block|
         | 
| 149 | 
            +
                        collection_args.map do |args|
         | 
| 150 | 
            +
                          set_slot(slot_name, nil, **args, &block)
         | 
| 151 | 
            +
                        end
         | 
| 152 | 
            +
                      end
         | 
| 22 153 |  | 
| 154 | 
            +
                      define_method slot_name do |collection_args = nil, &block|
         | 
| 155 | 
            +
                        get_slot(slot_name)
         | 
| 156 | 
            +
                      end
         | 
| 157 | 
            +
             | 
| 158 | 
            +
                      define_method "#{slot_name}?" do
         | 
| 159 | 
            +
                        get_slot(slot_name).present?
         | 
| 160 | 
            +
                      end
         | 
| 161 | 
            +
             | 
| 162 | 
            +
                      register_slot(slot_name, collection: true, callable: callable)
         | 
| 163 | 
            +
                    end
         | 
| 164 | 
            +
                  end
         | 
| 165 | 
            +
             | 
| 166 | 
            +
                  def slot_type(slot_name)
         | 
| 167 | 
            +
                    registered_slot = registered_slots[slot_name]
         | 
| 168 | 
            +
                    if registered_slot
         | 
| 169 | 
            +
                      registered_slot[:collection] ? :collection : :single
         | 
| 170 | 
            +
                    else
         | 
| 171 | 
            +
                      plural_slot_name = ActiveSupport::Inflector.pluralize(slot_name).to_sym
         | 
| 172 | 
            +
                      plural_registered_slot = registered_slots[plural_slot_name]
         | 
| 173 | 
            +
                      plural_registered_slot&.fetch(:collection) ? :collection_item : nil
         | 
| 174 | 
            +
                    end
         | 
| 175 | 
            +
                  end
         | 
| 176 | 
            +
             | 
| 177 | 
            +
                  # Clone slot configuration into child class
         | 
| 178 | 
            +
                  # see #test_slots_pollution
         | 
| 179 | 
            +
                  def inherited(child)
         | 
| 180 | 
            +
                    child.registered_slots = registered_slots.clone
         | 
| 23 181 | 
             
                    super
         | 
| 24 182 | 
             
                  end
         | 
| 25 | 
            -
                end
         | 
| 26 183 |  | 
| 27 | 
            -
             | 
| 28 | 
            -
             | 
| 29 | 
            -
             | 
| 30 | 
            -
             | 
| 31 | 
            -
             | 
| 32 | 
            -
             | 
| 33 | 
            -
             | 
| 34 | 
            -
             | 
| 35 | 
            -
             | 
| 36 | 
            -
             | 
| 37 | 
            -
             | 
| 38 | 
            -
             | 
| 39 | 
            -
             | 
| 40 | 
            -
             | 
| 41 | 
            -
             | 
| 42 | 
            -
             | 
| 43 | 
            -
             | 
| 44 | 
            -
             | 
| 184 | 
            +
                  def register_polymorphic_slot(slot_name, types, collection:)
         | 
| 185 | 
            +
                    unless types.empty?
         | 
| 186 | 
            +
                      getter_name = slot_name
         | 
| 187 | 
            +
             | 
| 188 | 
            +
                      define_method(getter_name) do
         | 
| 189 | 
            +
                        get_slot(slot_name)
         | 
| 190 | 
            +
                      end
         | 
| 191 | 
            +
             | 
| 192 | 
            +
                      define_method("#{getter_name}?") do
         | 
| 193 | 
            +
                        get_slot(slot_name).present?
         | 
| 194 | 
            +
                      end
         | 
| 195 | 
            +
                    end
         | 
| 196 | 
            +
             | 
| 197 | 
            +
                    renderable_hash = types.each_with_object({}) do |(poly_type, poly_callable), memo|
         | 
| 198 | 
            +
                      memo[poly_type] = define_slot(
         | 
| 199 | 
            +
                        "#{slot_name}_#{poly_type}", collection: collection, callable: poly_callable
         | 
| 200 | 
            +
                      )
         | 
| 201 | 
            +
             | 
| 202 | 
            +
                      setter_name =
         | 
| 203 | 
            +
                        if collection
         | 
| 204 | 
            +
                          "#{ActiveSupport::Inflector.singularize(slot_name)}_#{poly_type}"
         | 
| 205 | 
            +
                        else
         | 
| 206 | 
            +
                          "#{slot_name}_#{poly_type}"
         | 
| 207 | 
            +
                        end
         | 
| 208 | 
            +
             | 
| 209 | 
            +
                      define_method("with_#{setter_name}") do |*args, &block|
         | 
| 210 | 
            +
                        set_polymorphic_slot(slot_name, poly_type, *args, &block)
         | 
| 211 | 
            +
                      end
         | 
| 212 | 
            +
                      ruby2_keywords(:"with_#{setter_name}") if respond_to?(:ruby2_keywords, true)
         | 
| 213 | 
            +
                    end
         | 
| 214 | 
            +
             | 
| 215 | 
            +
                    registered_slots[slot_name] = {
         | 
| 216 | 
            +
                      collection: collection,
         | 
| 217 | 
            +
                      renderable_hash: renderable_hash
         | 
| 218 | 
            +
                    }
         | 
| 45 219 | 
             
                  end
         | 
| 46 220 |  | 
| 47 | 
            -
                   | 
| 221 | 
            +
                  private
         | 
| 222 | 
            +
             | 
| 223 | 
            +
                  def register_slot(slot_name, **kwargs)
         | 
| 224 | 
            +
                    registered_slots[slot_name] = define_slot(slot_name, **kwargs)
         | 
| 225 | 
            +
                  end
         | 
| 48 226 |  | 
| 49 | 
            -
                   | 
| 50 | 
            -
             | 
| 227 | 
            +
                  def define_slot(slot_name, collection:, callable:)
         | 
| 228 | 
            +
                    # Setup basic slot data
         | 
| 229 | 
            +
                    slot = {
         | 
| 230 | 
            +
                      collection: collection
         | 
| 231 | 
            +
                    }
         | 
| 232 | 
            +
                    return slot unless callable
         | 
| 51 233 |  | 
| 52 | 
            -
             | 
| 53 | 
            -
                     | 
| 234 | 
            +
                    # If callable responds to `render_in`, we set it on the slot as a renderable
         | 
| 235 | 
            +
                    if callable.respond_to?(:method_defined?) && callable.method_defined?(:render_in)
         | 
| 236 | 
            +
                      slot[:renderable] = callable
         | 
| 237 | 
            +
                    elsif callable.is_a?(String)
         | 
| 238 | 
            +
                      # If callable is a string, we assume it's referencing an internal class
         | 
| 239 | 
            +
                      slot[:renderable_class_name] = callable
         | 
| 240 | 
            +
                    elsif callable.respond_to?(:call)
         | 
| 241 | 
            +
                      # If slot doesn't respond to `render_in`, we assume it's a proc,
         | 
| 242 | 
            +
                      # define a method, and save a reference to it to call when setting
         | 
| 243 | 
            +
                      method_name = :"_call_#{slot_name}"
         | 
| 244 | 
            +
                      define_method method_name, &callable
         | 
| 245 | 
            +
                      slot[:renderable_function] = instance_method(method_name)
         | 
| 246 | 
            +
                    else
         | 
| 247 | 
            +
                      raise(
         | 
| 248 | 
            +
                        ArgumentError,
         | 
| 249 | 
            +
                        "invalid slot definition. Please pass a class, string, or callable (i.e. proc, lambda, etc)"
         | 
| 250 | 
            +
                      )
         | 
| 251 | 
            +
                    end
         | 
| 252 | 
            +
             | 
| 253 | 
            +
                    slot
         | 
| 254 | 
            +
                  end
         | 
| 255 | 
            +
             | 
| 256 | 
            +
                  def validate_plural_slot_name(slot_name)
         | 
| 257 | 
            +
                    if RESERVED_NAMES[:plural].include?(slot_name.to_sym)
         | 
| 258 | 
            +
                      raise ArgumentError.new(
         | 
| 259 | 
            +
                        "#{self} declares a slot named #{slot_name}, which is a reserved word in the ViewComponent framework.\n\n" \
         | 
| 260 | 
            +
                        "To fix this issue, choose a different name."
         | 
| 261 | 
            +
                      )
         | 
| 262 | 
            +
                    end
         | 
| 263 | 
            +
             | 
| 264 | 
            +
                    raise_if_slot_ends_with_question_mark(slot_name)
         | 
| 265 | 
            +
                    raise_if_slot_registered(slot_name)
         | 
| 266 | 
            +
                  end
         | 
| 267 | 
            +
             | 
| 268 | 
            +
                  def validate_singular_slot_name(slot_name)
         | 
| 269 | 
            +
                    if slot_name.to_sym == :content
         | 
| 270 | 
            +
                      raise ArgumentError.new(
         | 
| 271 | 
            +
                        "#{self} declares a slot named content, which is a reserved word in ViewComponent.\n\n" \
         | 
| 272 | 
            +
                        "Content passed to a ViewComponent as a block is captured and assigned to the `content` accessor without having to create an explicit slot.\n\n" \
         | 
| 273 | 
            +
                        "To fix this issue, either use the `content` accessor directly or choose a different slot name."
         | 
| 274 | 
            +
                      )
         | 
| 275 | 
            +
                    end
         | 
| 276 | 
            +
             | 
| 277 | 
            +
                    if RESERVED_NAMES[:singular].include?(slot_name.to_sym)
         | 
| 278 | 
            +
                      raise ArgumentError.new(
         | 
| 279 | 
            +
                        "#{self} declares a slot named #{slot_name}, which is a reserved word in the ViewComponent framework.\n\n" \
         | 
| 280 | 
            +
                        "To fix this issue, choose a different name."
         | 
| 281 | 
            +
                      )
         | 
| 282 | 
            +
                    end
         | 
| 283 | 
            +
             | 
| 284 | 
            +
                    raise_if_slot_ends_with_question_mark(slot_name)
         | 
| 285 | 
            +
                    raise_if_slot_registered(slot_name)
         | 
| 286 | 
            +
                  end
         | 
| 287 | 
            +
             | 
| 288 | 
            +
                  def raise_if_slot_registered(slot_name)
         | 
| 289 | 
            +
                    if registered_slots.key?(slot_name)
         | 
| 290 | 
            +
                      # TODO remove? This breaks overriding slots when slots are inherited
         | 
| 291 | 
            +
                      raise ArgumentError.new(
         | 
| 292 | 
            +
                        "#{self} declares the #{slot_name} slot multiple times.\n\n" \
         | 
| 293 | 
            +
                        "To fix this issue, choose a different slot name."
         | 
| 294 | 
            +
                      )
         | 
| 295 | 
            +
                    end
         | 
| 296 | 
            +
                  end
         | 
| 297 | 
            +
             | 
| 298 | 
            +
                  def raise_if_slot_ends_with_question_mark(slot_name)
         | 
| 299 | 
            +
                    if slot_name.to_s.ends_with?("?")
         | 
| 300 | 
            +
                      raise ArgumentError.new(
         | 
| 301 | 
            +
                        "#{self} declares a slot named #{slot_name}, which ends with a question mark.\n\n" \
         | 
| 302 | 
            +
                        "This is not allowed because the ViewComponent framework already provides predicate " \
         | 
| 303 | 
            +
                        "methods ending in `?`.\n\n" \
         | 
| 304 | 
            +
                        "To fix this issue, choose a different name."
         | 
| 305 | 
            +
                      )
         | 
| 306 | 
            +
                    end
         | 
| 54 307 | 
             
                  end
         | 
| 308 | 
            +
                end
         | 
| 309 | 
            +
             | 
| 310 | 
            +
                def get_slot(slot_name)
         | 
| 311 | 
            +
                  content unless content_evaluated? # ensure content is loaded so slots will be defined
         | 
| 55 312 |  | 
| 56 | 
            -
                   | 
| 57 | 
            -
                   | 
| 313 | 
            +
                  slot = self.class.registered_slots[slot_name]
         | 
| 314 | 
            +
                  @__vc_set_slots ||= {}
         | 
| 58 315 |  | 
| 59 | 
            -
                   | 
| 60 | 
            -
             | 
| 316 | 
            +
                  if @__vc_set_slots[slot_name]
         | 
| 317 | 
            +
                    return @__vc_set_slots[slot_name]
         | 
| 318 | 
            +
                  end
         | 
| 61 319 |  | 
| 62 320 | 
             
                  if slot[:collection]
         | 
| 63 | 
            -
                     | 
| 64 | 
            -
             | 
| 65 | 
            -
             | 
| 66 | 
            -
             | 
| 321 | 
            +
                    []
         | 
| 322 | 
            +
                  end
         | 
| 323 | 
            +
                end
         | 
| 324 | 
            +
             | 
| 325 | 
            +
                def set_slot(slot_name, slot_definition = nil, *args, &block)
         | 
| 326 | 
            +
                  slot_definition ||= self.class.registered_slots[slot_name]
         | 
| 327 | 
            +
                  slot = Slot.new(self)
         | 
| 328 | 
            +
             | 
| 329 | 
            +
                  # Passing the block to the sub-component wrapper like this has two
         | 
| 330 | 
            +
                  # benefits:
         | 
| 331 | 
            +
                  #
         | 
| 332 | 
            +
                  # 1. If this is a `content_area` style sub-component, we will render the
         | 
| 333 | 
            +
                  # block via the `slot`
         | 
| 334 | 
            +
                  #
         | 
| 335 | 
            +
                  # 2. Since we've to pass block content to components when calling
         | 
| 336 | 
            +
                  # `render`, evaluating the block here would require us to call
         | 
| 337 | 
            +
                  # `view_context.capture` twice, which is slower
         | 
| 338 | 
            +
                  slot.__vc_content_block = block if block
         | 
| 339 | 
            +
             | 
| 340 | 
            +
                  # If class
         | 
| 341 | 
            +
                  if slot_definition[:renderable]
         | 
| 342 | 
            +
                    slot.__vc_component_instance = slot_definition[:renderable].new(*args)
         | 
| 343 | 
            +
                  # If class name as a string
         | 
| 344 | 
            +
                  elsif slot_definition[:renderable_class_name]
         | 
| 345 | 
            +
                    slot.__vc_component_instance =
         | 
| 346 | 
            +
                      self.class.const_get(slot_definition[:renderable_class_name]).new(*args)
         | 
| 347 | 
            +
                  # If passed a lambda
         | 
| 348 | 
            +
                  elsif slot_definition[:renderable_function]
         | 
| 349 | 
            +
                    # Use `bind(self)` to ensure lambda is executed in the context of the
         | 
| 350 | 
            +
                    # current component. This is necessary to allow the lambda to access helper
         | 
| 351 | 
            +
                    # methods like `content_tag` as well as parent component state.
         | 
| 352 | 
            +
                    renderable_function = slot_definition[:renderable_function].bind(self)
         | 
| 353 | 
            +
                    renderable_value =
         | 
| 354 | 
            +
                      if block
         | 
| 355 | 
            +
                        renderable_function.call(*args) do |*rargs|
         | 
| 356 | 
            +
                          view_context.capture(*rargs, &block)
         | 
| 357 | 
            +
                        end
         | 
| 358 | 
            +
                      else
         | 
| 359 | 
            +
                        renderable_function.call(*args)
         | 
| 360 | 
            +
                      end
         | 
| 361 | 
            +
             | 
| 362 | 
            +
                    # Function calls can return components, so if it's a component handle it specially
         | 
| 363 | 
            +
                    if renderable_value.respond_to?(:render_in)
         | 
| 364 | 
            +
                      slot.__vc_component_instance = renderable_value
         | 
| 365 | 
            +
                    else
         | 
| 366 | 
            +
                      slot.__vc_content = renderable_value
         | 
| 67 367 | 
             
                    end
         | 
| 368 | 
            +
                  end
         | 
| 369 | 
            +
             | 
| 370 | 
            +
                  @__vc_set_slots ||= {}
         | 
| 68 371 |  | 
| 69 | 
            -
             | 
| 70 | 
            -
                     | 
| 372 | 
            +
                  if slot_definition[:collection]
         | 
| 373 | 
            +
                    @__vc_set_slots[slot_name] ||= []
         | 
| 374 | 
            +
                    @__vc_set_slots[slot_name].push(slot)
         | 
| 71 375 | 
             
                  else
         | 
| 72 | 
            -
                     | 
| 73 | 
            -
             | 
| 376 | 
            +
                    @__vc_set_slots[slot_name] = slot
         | 
| 377 | 
            +
                  end
         | 
| 378 | 
            +
             | 
| 379 | 
            +
                  slot
         | 
| 380 | 
            +
                end
         | 
| 381 | 
            +
                ruby2_keywords(:set_slot) if respond_to?(:ruby2_keywords, true)
         | 
| 382 | 
            +
             | 
| 383 | 
            +
                def set_polymorphic_slot(slot_name, poly_type = nil, *args, &block)
         | 
| 384 | 
            +
                  slot_definition = self.class.registered_slots[slot_name]
         | 
| 385 | 
            +
             | 
| 386 | 
            +
                  if !slot_definition[:collection] && (defined?(@__vc_set_slots) && @__vc_set_slots[slot_name])
         | 
| 387 | 
            +
                    raise ArgumentError, "content for slot '#{slot_name}' has already been provided"
         | 
| 74 388 | 
             
                  end
         | 
| 75 389 |  | 
| 76 | 
            -
                   | 
| 77 | 
            -
             | 
| 390 | 
            +
                  poly_def = slot_definition[:renderable_hash][poly_type]
         | 
| 391 | 
            +
             | 
| 392 | 
            +
                  set_slot(slot_name, poly_def, *args, &block)
         | 
| 78 393 | 
             
                end
         | 
| 394 | 
            +
                ruby2_keywords(:set_polymorphic_slot) if respond_to?(:ruby2_keywords, true)
         | 
| 79 395 | 
             
              end
         | 
| 80 396 | 
             
            end
         | 
| @@ -15,7 +15,7 @@ module ViewComponent | |
| 15 15 |  | 
| 16 16 | 
             
                  file = Tempfile.new(["rendered_#{fragment.class.name}", ".html"], "tmp/view_components/")
         | 
| 17 17 | 
             
                  begin
         | 
| 18 | 
            -
                    file.write( | 
| 18 | 
            +
                    file.write(__vc_test_helpers_controller.render_to_string(html: fragment.to_html.html_safe, layout: layout))
         | 
| 19 19 | 
             
                    file.rewind
         | 
| 20 20 |  | 
| 21 21 | 
             
                    block.call("/_system_test_entrypoint?file=#{file.path.split("/").last}")
         | 
| @@ -28,7 +28,9 @@ module ViewComponent | |
| 28 28 | 
             
                  # :nocov:
         | 
| 29 29 | 
             
                end
         | 
| 30 30 |  | 
| 31 | 
            -
                #  | 
| 31 | 
            +
                # Returns the result of a render_inline call.
         | 
| 32 | 
            +
                #
         | 
| 33 | 
            +
                # @return [ActionView::OutputBuffer]
         | 
| 32 34 | 
             
                attr_reader :rendered_content
         | 
| 33 35 |  | 
| 34 36 | 
             
                # Render a component inline. Internally sets `page` to be a `Capybara::Node::Simple`,
         | 
| @@ -45,9 +47,9 @@ module ViewComponent | |
| 45 47 | 
             
                  @page = nil
         | 
| 46 48 | 
             
                  @rendered_content =
         | 
| 47 49 | 
             
                    if Rails.version.to_f >= 6.1
         | 
| 48 | 
            -
                       | 
| 50 | 
            +
                      __vc_test_helpers_controller.view_context.render(component, args, &block)
         | 
| 49 51 | 
             
                    else
         | 
| 50 | 
            -
                       | 
| 52 | 
            +
                      __vc_test_helpers_controller.view_context.render_component(component, &block)
         | 
| 51 53 | 
             
                    end
         | 
| 52 54 |  | 
| 53 55 | 
             
                  Nokogiri::HTML.fragment(@rendered_content)
         | 
| @@ -72,8 +74,8 @@ module ViewComponent | |
| 72 74 | 
             
                # @param from [ViewComponent::Preview] The class of the preview to be rendered.
         | 
| 73 75 | 
             
                # @param params [Hash] Parameters to be passed to the preview.
         | 
| 74 76 | 
             
                # @return [Nokogiri::HTML]
         | 
| 75 | 
            -
                def render_preview(name, from:  | 
| 76 | 
            -
                  previews_controller =  | 
| 77 | 
            +
                def render_preview(name, from: __vc_test_helpers_preview_class, params: {})
         | 
| 78 | 
            +
                  previews_controller = __vc_test_helpers_build_controller(Rails.application.config.view_component.preview_controller.constantize)
         | 
| 77 79 |  | 
| 78 80 | 
             
                  # From what I can tell, it's not possible to overwrite all request parameters
         | 
| 79 81 | 
             
                  # at once, so we set them individually here.
         | 
| @@ -103,26 +105,11 @@ module ViewComponent | |
| 103 105 | 
             
                # ```
         | 
| 104 106 | 
             
                def render_in_view_context(*args, &block)
         | 
| 105 107 | 
             
                  @page = nil
         | 
| 106 | 
            -
                  @rendered_content =  | 
| 108 | 
            +
                  @rendered_content = __vc_test_helpers_controller.view_context.instance_exec(*args, &block)
         | 
| 107 109 | 
             
                  Nokogiri::HTML.fragment(@rendered_content)
         | 
| 108 110 | 
             
                end
         | 
| 109 111 | 
             
                ruby2_keywords(:render_in_view_context) if respond_to?(:ruby2_keywords, true)
         | 
| 110 112 |  | 
| 111 | 
            -
                # @private
         | 
| 112 | 
            -
                def controller
         | 
| 113 | 
            -
                  @controller ||= build_controller(Base.test_controller.constantize)
         | 
| 114 | 
            -
                end
         | 
| 115 | 
            -
             | 
| 116 | 
            -
                # @private
         | 
| 117 | 
            -
                def request
         | 
| 118 | 
            -
                  @request ||=
         | 
| 119 | 
            -
                    begin
         | 
| 120 | 
            -
                      request = ActionDispatch::TestRequest.create
         | 
| 121 | 
            -
                      request.session = ActionController::TestSession.new
         | 
| 122 | 
            -
                      request
         | 
| 123 | 
            -
                    end
         | 
| 124 | 
            -
                end
         | 
| 125 | 
            -
             | 
| 126 113 | 
             
                # Set the Action Pack request variant for the given block:
         | 
| 127 114 | 
             
                #
         | 
| 128 115 | 
             
                # ```ruby
         | 
| @@ -133,12 +120,12 @@ module ViewComponent | |
| 133 120 | 
             
                #
         | 
| 134 121 | 
             
                # @param variant [Symbol] The variant to be set for the provided block.
         | 
| 135 122 | 
             
                def with_variant(variant)
         | 
| 136 | 
            -
                  old_variants =  | 
| 123 | 
            +
                  old_variants = __vc_test_helpers_controller.view_context.lookup_context.variants
         | 
| 137 124 |  | 
| 138 | 
            -
                   | 
| 125 | 
            +
                  __vc_test_helpers_controller.view_context.lookup_context.variants = variant
         | 
| 139 126 | 
             
                  yield
         | 
| 140 127 | 
             
                ensure
         | 
| 141 | 
            -
                   | 
| 128 | 
            +
                  __vc_test_helpers_controller.view_context.lookup_context.variants = old_variants
         | 
| 142 129 | 
             
                end
         | 
| 143 130 |  | 
| 144 131 | 
             
                # Set the controller to be used while executing the given block,
         | 
| @@ -152,12 +139,12 @@ module ViewComponent | |
| 152 139 | 
             
                #
         | 
| 153 140 | 
             
                # @param klass [ActionController::Base] The controller to be used.
         | 
| 154 141 | 
             
                def with_controller_class(klass)
         | 
| 155 | 
            -
                  old_controller = defined?(@ | 
| 142 | 
            +
                  old_controller = defined?(@__vc_test_helpers_controller) && @__vc_test_helpers_controller
         | 
| 156 143 |  | 
| 157 | 
            -
                  @ | 
| 144 | 
            +
                  @__vc_test_helpers_controller = __vc_test_helpers_build_controller(klass)
         | 
| 158 145 | 
             
                  yield
         | 
| 159 146 | 
             
                ensure
         | 
| 160 | 
            -
                  @ | 
| 147 | 
            +
                  @__vc_test_helpers_controller = old_controller
         | 
| 161 148 | 
             
                end
         | 
| 162 149 |  | 
| 163 150 | 
             
                # Set the URL of the current request (such as when using request-dependent path helpers):
         | 
| @@ -170,34 +157,47 @@ module ViewComponent | |
| 170 157 | 
             
                #
         | 
| 171 158 | 
             
                # @param path [String] The path to set for the current request.
         | 
| 172 159 | 
             
                def with_request_url(path)
         | 
| 173 | 
            -
                  old_request_path_info =  | 
| 174 | 
            -
                  old_request_path_parameters =  | 
| 175 | 
            -
                  old_request_query_parameters =  | 
| 176 | 
            -
                  old_request_query_string =  | 
| 177 | 
            -
                  old_controller = defined?(@ | 
| 160 | 
            +
                  old_request_path_info = __vc_test_helpers_request.path_info
         | 
| 161 | 
            +
                  old_request_path_parameters = __vc_test_helpers_request.path_parameters
         | 
| 162 | 
            +
                  old_request_query_parameters = __vc_test_helpers_request.query_parameters
         | 
| 163 | 
            +
                  old_request_query_string = __vc_test_helpers_request.query_string
         | 
| 164 | 
            +
                  old_controller = defined?(@__vc_test_helpers_controller) && @__vc_test_helpers_controller
         | 
| 178 165 |  | 
| 179 166 | 
             
                  path, query = path.split("?", 2)
         | 
| 180 | 
            -
                   | 
| 181 | 
            -
                   | 
| 182 | 
            -
                   | 
| 183 | 
            -
                   | 
| 167 | 
            +
                  __vc_test_helpers_request.path_info = path
         | 
| 168 | 
            +
                  __vc_test_helpers_request.path_parameters = Rails.application.routes.recognize_path_with_request(__vc_test_helpers_request, path, {})
         | 
| 169 | 
            +
                  __vc_test_helpers_request.set_header("action_dispatch.request.query_parameters", Rack::Utils.parse_nested_query(query))
         | 
| 170 | 
            +
                  __vc_test_helpers_request.set_header(Rack::QUERY_STRING, query)
         | 
| 184 171 | 
             
                  yield
         | 
| 185 172 | 
             
                ensure
         | 
| 186 | 
            -
                   | 
| 187 | 
            -
                   | 
| 188 | 
            -
                   | 
| 189 | 
            -
                   | 
| 190 | 
            -
                  @ | 
| 173 | 
            +
                  __vc_test_helpers_request.path_info = old_request_path_info
         | 
| 174 | 
            +
                  __vc_test_helpers_request.path_parameters = old_request_path_parameters
         | 
| 175 | 
            +
                  __vc_test_helpers_request.set_header("action_dispatch.request.query_parameters", old_request_query_parameters)
         | 
| 176 | 
            +
                  __vc_test_helpers_request.set_header(Rack::QUERY_STRING, old_request_query_string)
         | 
| 177 | 
            +
                  @__vc_test_helpers_controller = old_controller
         | 
| 191 178 | 
             
                end
         | 
| 192 179 |  | 
| 193 | 
            -
                #  | 
| 194 | 
            -
                 | 
| 195 | 
            -
             | 
| 180 | 
            +
                # Note: We prefix private methods here to prevent collisions in consumer's tests.
         | 
| 181 | 
            +
                private
         | 
| 182 | 
            +
             | 
| 183 | 
            +
                def __vc_test_helpers_controller
         | 
| 184 | 
            +
                  @__vc_test_helpers_controller ||= __vc_test_helpers_build_controller(Base.test_controller.constantize)
         | 
| 196 185 | 
             
                end
         | 
| 197 186 |  | 
| 198 | 
            -
                 | 
| 187 | 
            +
                def __vc_test_helpers_request
         | 
| 188 | 
            +
                  @__vc_test_helpers_request ||=
         | 
| 189 | 
            +
                    begin
         | 
| 190 | 
            +
                      out = ActionDispatch::TestRequest.create
         | 
| 191 | 
            +
                      out.session = ActionController::TestSession.new
         | 
| 192 | 
            +
                      out
         | 
| 193 | 
            +
                    end
         | 
| 194 | 
            +
                end
         | 
| 195 | 
            +
             | 
| 196 | 
            +
                def __vc_test_helpers_build_controller(klass)
         | 
| 197 | 
            +
                  klass.new.tap { |c| c.request = __vc_test_helpers_request }.extend(Rails.application.routes.url_helpers)
         | 
| 198 | 
            +
                end
         | 
| 199 199 |  | 
| 200 | 
            -
                def  | 
| 200 | 
            +
                def __vc_test_helpers_preview_class
         | 
| 201 201 | 
             
                  result = if respond_to?(:described_class)
         | 
| 202 202 | 
             
                    raise "`render_preview` expected a described_class, but it is nil." if described_class.nil?
         | 
| 203 203 |  | 
    
        data/lib/view_component.rb
    CHANGED
    
    
    
        metadata
    CHANGED
    
    | @@ -1,14 +1,14 @@ | |
| 1 1 | 
             
            --- !ruby/object:Gem::Specification
         | 
| 2 2 | 
             
            name: view_component
         | 
| 3 3 | 
             
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            -
              version: 3.0.0. | 
| 4 | 
            +
              version: 3.0.0.rc2
         | 
| 5 5 | 
             
            platform: ruby
         | 
| 6 6 | 
             
            authors:
         | 
| 7 7 | 
             
            - ViewComponent Team
         | 
| 8 8 | 
             
            autorequire:
         | 
| 9 9 | 
             
            bindir: bin
         | 
| 10 10 | 
             
            cert_chain: []
         | 
| 11 | 
            -
            date: 2023- | 
| 11 | 
            +
            date: 2023-02-17 00:00:00.000000000 Z
         | 
| 12 12 | 
             
            dependencies:
         | 
| 13 13 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 14 14 | 
             
              name: activesupport
         | 
| @@ -353,6 +353,7 @@ files: | |
| 353 353 | 
             
            - lib/rails/generators/test_unit/templates/component_test.rb.tt
         | 
| 354 354 | 
             
            - lib/view_component.rb
         | 
| 355 355 | 
             
            - lib/view_component/base.rb
         | 
| 356 | 
            +
            - lib/view_component/capture_compatibility.rb
         | 
| 356 357 | 
             
            - lib/view_component/collection.rb
         | 
| 357 358 | 
             
            - lib/view_component/compile_cache.rb
         | 
| 358 359 | 
             
            - lib/view_component/compiler.rb
         | 
| @@ -363,7 +364,6 @@ files: | |
| 363 364 | 
             
            - lib/view_component/docs_builder_component.rb
         | 
| 364 365 | 
             
            - lib/view_component/engine.rb
         | 
| 365 366 | 
             
            - lib/view_component/instrumentation.rb
         | 
| 366 | 
            -
            - lib/view_component/polymorphic_slots.rb
         | 
| 367 367 | 
             
            - lib/view_component/preview.rb
         | 
| 368 368 | 
             
            - lib/view_component/preview_template_error.rb
         | 
| 369 369 | 
             
            - lib/view_component/rails/tasks/view_component.rake
         | 
| @@ -373,9 +373,8 @@ files: | |
| 373 373 | 
             
            - lib/view_component/render_to_string_monkey_patch.rb
         | 
| 374 374 | 
             
            - lib/view_component/rendering_component_helper.rb
         | 
| 375 375 | 
             
            - lib/view_component/rendering_monkey_patch.rb
         | 
| 376 | 
            -
            - lib/view_component/ | 
| 376 | 
            +
            - lib/view_component/slot.rb
         | 
| 377 377 | 
             
            - lib/view_component/slotable.rb
         | 
| 378 | 
            -
            - lib/view_component/slotable_v2.rb
         | 
| 379 378 | 
             
            - lib/view_component/system_test_case.rb
         | 
| 380 379 | 
             
            - lib/view_component/system_test_helpers.rb
         | 
| 381 380 | 
             
            - lib/view_component/template_error.rb
         |