view_component 2.19.1 → 2.23.0
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/CHANGELOG.md +44 -0
- data/README.md +4 -1106
- data/lib/rails/generators/component/templates/component.rb.tt +2 -0
- data/lib/rails/generators/test_unit/templates/component_test.rb.tt +1 -1
- data/lib/view_component.rb +1 -0
- data/lib/view_component/base.rb +26 -199
- data/lib/view_component/compiler.rb +229 -0
- data/lib/view_component/engine.rb +10 -5
- data/lib/view_component/preview.rb +4 -1
- data/lib/view_component/previewable.rb +10 -0
- data/lib/view_component/slot_v2.rb +65 -0
- data/lib/view_component/slotable.rb +14 -0
- data/lib/view_component/slotable_v2.rb +249 -0
- data/lib/view_component/version.rb +2 -2
- metadata +19 -2
@@ -14,9 +14,12 @@ module ViewComponent
|
|
14
14
|
options.render_monkey_patch_enabled = true if options.render_monkey_patch_enabled.nil?
|
15
15
|
options.show_previews = Rails.env.development? if options.show_previews.nil?
|
16
16
|
options.preview_route ||= ViewComponent::Base.preview_route
|
17
|
+
options.preview_controller ||= ViewComponent::Base.preview_controller
|
17
18
|
|
18
19
|
if options.show_previews
|
19
|
-
options.preview_paths << "#{Rails.root}/test/components/previews" if defined?(Rails.root)
|
20
|
+
options.preview_paths << "#{Rails.root}/test/components/previews" if defined?(Rails.root) && Dir.exist?(
|
21
|
+
"#{Rails.root}/test/components/previews"
|
22
|
+
)
|
20
23
|
|
21
24
|
if options.preview_path.present?
|
22
25
|
ActiveSupport::Deprecation.warn(
|
@@ -41,7 +44,7 @@ module ViewComponent
|
|
41
44
|
|
42
45
|
initializer "view_component.eager_load_actions" do
|
43
46
|
ActiveSupport.on_load(:after_initialize) do
|
44
|
-
ViewComponent::Base.descendants.each(&:compile)
|
47
|
+
ViewComponent::Base.descendants.each(&:compile) if Rails.application.config.eager_load
|
45
48
|
end
|
46
49
|
end
|
47
50
|
|
@@ -87,9 +90,11 @@ module ViewComponent
|
|
87
90
|
options = app.config.view_component
|
88
91
|
|
89
92
|
if options.show_previews
|
90
|
-
app.routes.
|
91
|
-
|
92
|
-
|
93
|
+
app.routes.append do
|
94
|
+
preview_controller = options.preview_controller.sub(/Controller$/, "").underscore
|
95
|
+
|
96
|
+
get options.preview_route, to: "#{preview_controller}#index", as: :preview_view_components, internal: true
|
97
|
+
get "#{options.preview_route}/*path", to: "#{preview_controller}#previews", as: :preview_view_component, internal: true
|
93
98
|
end
|
94
99
|
end
|
95
100
|
|
@@ -80,7 +80,10 @@ module ViewComponent # :nodoc:
|
|
80
80
|
end
|
81
81
|
|
82
82
|
path = Dir["#{preview_path}/#{preview_name}_preview/#{example}.html.*"].first
|
83
|
-
Pathname.new(path)
|
83
|
+
Pathname.new(path)
|
84
|
+
.relative_path_from(Pathname.new(preview_path))
|
85
|
+
.to_s
|
86
|
+
.sub(/\..*$/, "")
|
84
87
|
end
|
85
88
|
|
86
89
|
private
|
@@ -41,6 +41,16 @@ module ViewComponent # :nodoc:
|
|
41
41
|
mattr_accessor :preview_route, instance_writer: false do
|
42
42
|
"/rails/view_components"
|
43
43
|
end
|
44
|
+
|
45
|
+
# Set the controller to be used for previewing components through app configuration:
|
46
|
+
#
|
47
|
+
# config.view_component.preview_controller = "MyPreviewController"
|
48
|
+
#
|
49
|
+
# Defaults to the provided +ViewComponentsController+
|
50
|
+
#
|
51
|
+
mattr_accessor :preview_controller, instance_writer: false do
|
52
|
+
"ViewComponentsController"
|
53
|
+
end
|
44
54
|
end
|
45
55
|
end
|
46
56
|
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ViewComponent
|
4
|
+
class SlotV2
|
5
|
+
attr_writer :_component_instance, :_content_block, :_content
|
6
|
+
|
7
|
+
def initialize(parent)
|
8
|
+
@parent = parent
|
9
|
+
end
|
10
|
+
|
11
|
+
# Used to render the slot content in the template
|
12
|
+
#
|
13
|
+
# There's currently 3 different values that may be set, that we can render.
|
14
|
+
#
|
15
|
+
# If the slot renderable is a component, the string class name of a
|
16
|
+
# component, or a function that returns a component, we render that
|
17
|
+
# component instance, returning the string.
|
18
|
+
#
|
19
|
+
# If the slot renderable is a function and returns a string, it is
|
20
|
+
# set as `@_content` and is returned directly.
|
21
|
+
#
|
22
|
+
# If there is no slot renderable, we evaluate the block passed to
|
23
|
+
# the slot and return it.
|
24
|
+
def to_s
|
25
|
+
if defined?(@_component_instance)
|
26
|
+
# render_in is faster than `parent.render`
|
27
|
+
@_component_instance.render_in(
|
28
|
+
@parent.send(:view_context),
|
29
|
+
&@_content_block
|
30
|
+
)
|
31
|
+
elsif defined?(@_content)
|
32
|
+
@_content
|
33
|
+
elsif defined?(@_content_block)
|
34
|
+
@_content_block.call
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# Allow access to public component methods via the wrapper
|
39
|
+
#
|
40
|
+
# e.g.
|
41
|
+
#
|
42
|
+
# calling `header.name` (where `header` is a slot) will call `name`
|
43
|
+
# on the `HeaderComponent` instance.
|
44
|
+
#
|
45
|
+
# Where the component may look like:
|
46
|
+
#
|
47
|
+
# class MyComponent < ViewComponent::Base
|
48
|
+
# has_one :header, HeaderComponent
|
49
|
+
#
|
50
|
+
# class HeaderComponent < ViewComponent::Base
|
51
|
+
# def name
|
52
|
+
# @name
|
53
|
+
# end
|
54
|
+
# end
|
55
|
+
# end
|
56
|
+
#
|
57
|
+
def method_missing(symbol, *args, &block)
|
58
|
+
@_component_instance.public_send(symbol, *args, &block)
|
59
|
+
end
|
60
|
+
|
61
|
+
def respond_to_missing?(symbol, include_all = false)
|
62
|
+
@_component_instance.respond_to?(symbol, include_all)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -8,6 +8,12 @@ module ViewComponent
|
|
8
8
|
module Slotable
|
9
9
|
extend ActiveSupport::Concern
|
10
10
|
|
11
|
+
included do
|
12
|
+
# Hash of registered Slots
|
13
|
+
class_attribute :slots
|
14
|
+
self.slots = {}
|
15
|
+
end
|
16
|
+
|
11
17
|
class_methods do
|
12
18
|
# support initializing slots as:
|
13
19
|
#
|
@@ -63,6 +69,14 @@ module ViewComponent
|
|
63
69
|
}
|
64
70
|
end
|
65
71
|
end
|
72
|
+
|
73
|
+
def inherited(child)
|
74
|
+
# Clone slot configuration into child class
|
75
|
+
# see #test_slots_pollution
|
76
|
+
child.slots = self.slots.clone
|
77
|
+
|
78
|
+
super
|
79
|
+
end
|
66
80
|
end
|
67
81
|
|
68
82
|
# Build a Slot instance on a component,
|
@@ -0,0 +1,249 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/concern"
|
4
|
+
|
5
|
+
require "view_component/slot_v2"
|
6
|
+
|
7
|
+
module ViewComponent
|
8
|
+
module SlotableV2
|
9
|
+
extend ActiveSupport::Concern
|
10
|
+
|
11
|
+
# Setup component slot state
|
12
|
+
included do
|
13
|
+
# Hash of registered Slots
|
14
|
+
class_attribute :registered_slots
|
15
|
+
self.registered_slots = {}
|
16
|
+
end
|
17
|
+
|
18
|
+
class_methods do
|
19
|
+
##
|
20
|
+
# Registers a sub-component
|
21
|
+
#
|
22
|
+
# = Example
|
23
|
+
#
|
24
|
+
# renders_one :header -> (classes:) do
|
25
|
+
# HeaderComponent.new(classes: classes)
|
26
|
+
# end
|
27
|
+
#
|
28
|
+
# # OR
|
29
|
+
#
|
30
|
+
# renders_one :header, HeaderComponent
|
31
|
+
#
|
32
|
+
# where `HeaderComponent` is defined as:
|
33
|
+
#
|
34
|
+
# class HeaderComponent < ViewComponent::Base
|
35
|
+
# def initialize(classes:)
|
36
|
+
# @classes = classes
|
37
|
+
# end
|
38
|
+
# end
|
39
|
+
#
|
40
|
+
# and has the following template:
|
41
|
+
#
|
42
|
+
# <header class="<%= @classes %>">
|
43
|
+
# <%= content %>
|
44
|
+
# </header>
|
45
|
+
#
|
46
|
+
# = Rendering sub-component content
|
47
|
+
#
|
48
|
+
# The component's sidecar template can access the sub-component by calling a
|
49
|
+
# helper method with the same name as the sub-component.
|
50
|
+
#
|
51
|
+
# <h1>
|
52
|
+
# <%= header do %>
|
53
|
+
# My header title
|
54
|
+
# <% end %>
|
55
|
+
# </h1>
|
56
|
+
#
|
57
|
+
# = Setting sub-component content
|
58
|
+
#
|
59
|
+
# Consumers of the component can render a sub-component by calling a
|
60
|
+
# helper method with the same name as the slot.
|
61
|
+
#
|
62
|
+
# <%= render_inline(MyComponent.new) do |component| %>
|
63
|
+
# <%= component.header(classes: "Foo") do %>
|
64
|
+
# <p>Bar</p>
|
65
|
+
# <% end %>
|
66
|
+
# <% end %>
|
67
|
+
def renders_one(slot_name, callable = nil)
|
68
|
+
validate_slot_name(slot_name)
|
69
|
+
|
70
|
+
define_method slot_name do |*args, **kwargs, &block|
|
71
|
+
if args.empty? && kwargs.empty? && block.nil?
|
72
|
+
get_slot(slot_name)
|
73
|
+
else
|
74
|
+
set_slot(slot_name, *args, **kwargs, &block)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
register_slot(slot_name, collection: false, callable: callable)
|
79
|
+
end
|
80
|
+
|
81
|
+
##
|
82
|
+
# Registers a collection sub-component
|
83
|
+
#
|
84
|
+
# = Example
|
85
|
+
#
|
86
|
+
# render_many :items, -> (name:) { ItemComponent.new(name: name }
|
87
|
+
#
|
88
|
+
# # OR
|
89
|
+
#
|
90
|
+
# render_many :items, ItemComponent
|
91
|
+
#
|
92
|
+
# = Rendering sub-components
|
93
|
+
#
|
94
|
+
# The component's sidecar template can access the slot by calling a
|
95
|
+
# helper method with the same name as the slot.
|
96
|
+
#
|
97
|
+
# <h1>
|
98
|
+
# <%= items.each do |item| %>
|
99
|
+
# <%= item %>
|
100
|
+
# <% end %>
|
101
|
+
# </h1>
|
102
|
+
#
|
103
|
+
# = Setting sub-component content
|
104
|
+
#
|
105
|
+
# Consumers of the component can set the content of a slot by calling a
|
106
|
+
# helper method with the same name as the slot. The method can be
|
107
|
+
# called multiple times to append to the slot.
|
108
|
+
#
|
109
|
+
# <%= render_inline(MyComponent.new) do |component| %>
|
110
|
+
# <%= component.item(name: "Foo") do %>
|
111
|
+
# <p>One</p>
|
112
|
+
# <% end %>
|
113
|
+
#
|
114
|
+
# <%= component.item(name: "Bar") do %>
|
115
|
+
# <p>two</p>
|
116
|
+
# <% end %>
|
117
|
+
# <% end %>
|
118
|
+
def renders_many(slot_name, callable = nil)
|
119
|
+
validate_slot_name(slot_name)
|
120
|
+
|
121
|
+
singular_name = ActiveSupport::Inflector.singularize(slot_name)
|
122
|
+
|
123
|
+
# Define setter for singular names
|
124
|
+
# e.g. `renders_many :items` allows fetching all tabs with
|
125
|
+
# `component.tabs` and setting a tab with `component.tab`
|
126
|
+
define_method singular_name do |*args, **kwargs, &block|
|
127
|
+
set_slot(slot_name, *args, **kwargs, &block)
|
128
|
+
end
|
129
|
+
|
130
|
+
# Instantiates and and adds multiple slots forwarding the first
|
131
|
+
# argument to each slot constructor
|
132
|
+
define_method slot_name do |collection_args = nil, &block|
|
133
|
+
if collection_args.nil? && block.nil?
|
134
|
+
get_slot(slot_name)
|
135
|
+
else
|
136
|
+
collection_args.each do |args|
|
137
|
+
set_slot(slot_name, **args, &block)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
register_slot(slot_name, collection: true, callable: callable)
|
143
|
+
end
|
144
|
+
|
145
|
+
# Clone slot configuration into child class
|
146
|
+
# see #test_slots_pollution
|
147
|
+
def inherited(child)
|
148
|
+
child.registered_slots = self.registered_slots.clone
|
149
|
+
super
|
150
|
+
end
|
151
|
+
|
152
|
+
private
|
153
|
+
|
154
|
+
def register_slot(slot_name, collection:, callable:)
|
155
|
+
# Setup basic slot data
|
156
|
+
slot = {
|
157
|
+
collection: collection,
|
158
|
+
}
|
159
|
+
# If callable responds to `render_in`, we set it on the slot as a renderable
|
160
|
+
if callable && callable.respond_to?(:method_defined?) && callable.method_defined?(:render_in)
|
161
|
+
slot[:renderable] = callable
|
162
|
+
elsif callable.is_a?(String)
|
163
|
+
# If callable is a string, we assume it's referencing an internal class
|
164
|
+
slot[:renderable_class_name] = callable
|
165
|
+
elsif callable
|
166
|
+
# If slot does not respond to `render_in`, we assume it's a proc,
|
167
|
+
# define a method, and save a reference to it to call when setting
|
168
|
+
method_name = :"_call_#{slot_name}"
|
169
|
+
define_method method_name, &callable
|
170
|
+
slot[:renderable_function] = instance_method(method_name)
|
171
|
+
end
|
172
|
+
|
173
|
+
# Register the slot on the component
|
174
|
+
self.registered_slots[slot_name] = slot
|
175
|
+
end
|
176
|
+
|
177
|
+
def validate_slot_name(slot_name)
|
178
|
+
if self.registered_slots.key?(slot_name)
|
179
|
+
# TODO remove? This breaks overriding slots when slots are inherited
|
180
|
+
raise ArgumentError.new("#{slot_name} slot declared multiple times")
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
def get_slot(slot_name)
|
186
|
+
slot = self.class.registered_slots[slot_name]
|
187
|
+
@_set_slots ||= {}
|
188
|
+
|
189
|
+
if @_set_slots[slot_name]
|
190
|
+
return @_set_slots[slot_name]
|
191
|
+
end
|
192
|
+
|
193
|
+
if slot[:collection]
|
194
|
+
[]
|
195
|
+
else
|
196
|
+
nil
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
def set_slot(slot_name, *args, **kwargs, &block)
|
201
|
+
slot_definition = self.class.registered_slots[slot_name]
|
202
|
+
|
203
|
+
slot = SlotV2.new(self)
|
204
|
+
|
205
|
+
# Passing the block to the sub-component wrapper like this has two
|
206
|
+
# benefits:
|
207
|
+
#
|
208
|
+
# 1. If this is a `content_area` style sub-component, we will render the
|
209
|
+
# block via the `slot`
|
210
|
+
#
|
211
|
+
# 2. Since we have to pass block content to components when calling
|
212
|
+
# `render`, evaluating the block here would require us to call
|
213
|
+
# `view_context.capture` twice, which is slower
|
214
|
+
slot._content_block = block if block_given?
|
215
|
+
|
216
|
+
# If class
|
217
|
+
if slot_definition[:renderable]
|
218
|
+
slot._component_instance = slot_definition[:renderable].new(*args, **kwargs)
|
219
|
+
# If class name as a string
|
220
|
+
elsif slot_definition[:renderable_class_name]
|
221
|
+
slot._component_instance = self.class.const_get(slot_definition[:renderable_class_name]).new(*args, **kwargs)
|
222
|
+
# If passed a lambda
|
223
|
+
elsif slot_definition[:renderable_function]
|
224
|
+
# Use `bind(self)` to ensure lambda is executed in the context of the
|
225
|
+
# current component. This is necessary to allow the lambda to access helper
|
226
|
+
# methods like `content_tag` as well as parent component state.
|
227
|
+
renderable_value = slot_definition[:renderable_function].bind(self).call(*args, **kwargs, &block)
|
228
|
+
|
229
|
+
# Function calls can return components, so if it's a component handle it specially
|
230
|
+
if renderable_value.respond_to?(:render_in)
|
231
|
+
slot._component_instance = renderable_value
|
232
|
+
else
|
233
|
+
slot._content = renderable_value
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
@_set_slots ||= {}
|
238
|
+
|
239
|
+
if slot_definition[:collection]
|
240
|
+
@_set_slots[slot_name] ||= []
|
241
|
+
@_set_slots[slot_name].push(slot)
|
242
|
+
else
|
243
|
+
@_set_slots[slot_name] = slot
|
244
|
+
end
|
245
|
+
|
246
|
+
nil
|
247
|
+
end
|
248
|
+
end
|
249
|
+
end
|
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: 2.
|
4
|
+
version: 2.23.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- GitHub Open Source
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-09
|
11
|
+
date: 2020-12-09 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -184,6 +184,20 @@ dependencies:
|
|
184
184
|
- - "~>"
|
185
185
|
- !ruby/object:Gem::Version
|
186
186
|
version: 0.7.2
|
187
|
+
- !ruby/object:Gem::Dependency
|
188
|
+
name: pry
|
189
|
+
requirement: !ruby/object:Gem::Requirement
|
190
|
+
requirements:
|
191
|
+
- - "~>"
|
192
|
+
- !ruby/object:Gem::Version
|
193
|
+
version: '0.13'
|
194
|
+
type: :development
|
195
|
+
prerelease: false
|
196
|
+
version_requirements: !ruby/object:Gem::Requirement
|
197
|
+
requirements:
|
198
|
+
- - "~>"
|
199
|
+
- !ruby/object:Gem::Version
|
200
|
+
version: '0.13'
|
187
201
|
description:
|
188
202
|
email:
|
189
203
|
- opensource+view_component@github.com
|
@@ -215,6 +229,7 @@ files:
|
|
215
229
|
- lib/view_component/base.rb
|
216
230
|
- lib/view_component/collection.rb
|
217
231
|
- lib/view_component/compile_cache.rb
|
232
|
+
- lib/view_component/compiler.rb
|
218
233
|
- lib/view_component/engine.rb
|
219
234
|
- lib/view_component/preview.rb
|
220
235
|
- lib/view_component/preview_template_error.rb
|
@@ -226,7 +241,9 @@ files:
|
|
226
241
|
- lib/view_component/rendering_component_helper.rb
|
227
242
|
- lib/view_component/rendering_monkey_patch.rb
|
228
243
|
- lib/view_component/slot.rb
|
244
|
+
- lib/view_component/slot_v2.rb
|
229
245
|
- lib/view_component/slotable.rb
|
246
|
+
- lib/view_component/slotable_v2.rb
|
230
247
|
- lib/view_component/template_error.rb
|
231
248
|
- lib/view_component/test_case.rb
|
232
249
|
- lib/view_component/test_helpers.rb
|