view_component 2.11.1 → 2.12.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of view_component might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/CHANGELOG.md +6 -0
- data/README.md +133 -5
- data/lib/view_component/base.rb +9 -0
- data/lib/view_component/slot.rb +7 -0
- data/lib/view_component/slotable.rb +121 -0
- data/lib/view_component/version.rb +2 -2
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8661a26312dc3649e30ee17fbc94f6fd73be410067f8a7fac8b6aaefc4338d11
|
4
|
+
data.tar.gz: 1024034624c49bb65bfe1fc3466886ffa60cd9699a58c7427ea6dbedb9baa03b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 24ddc5373c2fbb7cd81ef4f4077ea66a30ea797fbb9d4c444135d3d8590b5151596fddc18f5bc5e1f18fec65ca87572858513409385489524c11c1929cff3567
|
7
|
+
data.tar.gz: cf63eb464e88e02310cb55f9c6c779c31622174777dbd1f39a24d0e9ac40407c555ac51012c5ea8c06bdeccac7aab880475c02c678c3fbff9ef22f5ca6cf797e
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -160,6 +160,133 @@ Returning:
|
|
160
160
|
</div>
|
161
161
|
```
|
162
162
|
|
163
|
+
#### Slots (experimental)
|
164
|
+
|
165
|
+
_Slots are currently under development as a successor to Content Areas. The Slot APIs should be considered unfinished and subject to breaking changes in non-major releases of ViewComponent._
|
166
|
+
|
167
|
+
Slots enable multiple blocks of content to be passed to a single ViewComponent.
|
168
|
+
|
169
|
+
Slots exist in two forms: normal slots and collection slots.
|
170
|
+
|
171
|
+
Normal slots can be rendered once per component. They expose an accessor with the name of the slot that returns an instance of `ViewComponent::Slot`, etc.
|
172
|
+
|
173
|
+
Collection slots can be rendered multiple times. They expose an accessor with the pluralized name of the slot (`#rows`), which is an Array of `ViewComponent::Slot` instances.
|
174
|
+
|
175
|
+
To learn more about the design of the Slots API, see https://github.com/github/view_component/pull/348.
|
176
|
+
|
177
|
+
##### Defining Slots
|
178
|
+
|
179
|
+
Slots are defined by the `with_slot` macro:
|
180
|
+
|
181
|
+
`with_slot :header`
|
182
|
+
|
183
|
+
To define a collection slot, add `collection: true`:
|
184
|
+
|
185
|
+
`with_slot :row, collection: true`
|
186
|
+
|
187
|
+
To define a slot with a custom class, pass `class_name`:
|
188
|
+
|
189
|
+
`with_slot :body, class_name: 'BodySlot`
|
190
|
+
|
191
|
+
Slot classes should be subclasses of `ViewComponent::Slot`.
|
192
|
+
|
193
|
+
##### Example ViewComponent with Slots
|
194
|
+
|
195
|
+
`# box_component.rb`
|
196
|
+
```ruby
|
197
|
+
class BoxComponent < ViewComponent::Base
|
198
|
+
include ViewComponent::Slotable
|
199
|
+
|
200
|
+
with_slot :body, :footer
|
201
|
+
with_slot :header, class_name: "Header"
|
202
|
+
with_slot :row, collection: true, class_name: "Row"
|
203
|
+
|
204
|
+
class Header < ViewComponent::Slot
|
205
|
+
def initialize(class_names: "")
|
206
|
+
@class_names = class_names
|
207
|
+
end
|
208
|
+
|
209
|
+
def class_names
|
210
|
+
"Box-header #{@class_names}"
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
class Row < ViewComponent::Slot
|
215
|
+
def initialize(theme: :gray)
|
216
|
+
@theme = theme
|
217
|
+
end
|
218
|
+
|
219
|
+
def theme_class_name
|
220
|
+
case @theme
|
221
|
+
when :gray
|
222
|
+
"Box-row--gray"
|
223
|
+
when :hover_gray
|
224
|
+
"Box-row--hover-gray"
|
225
|
+
when :yellow
|
226
|
+
"Box-row--yellow"
|
227
|
+
when :blue
|
228
|
+
"Box-row--blue"
|
229
|
+
when :hover_blue
|
230
|
+
"Box-row--hover-blue"
|
231
|
+
else
|
232
|
+
"Box-row--gray"
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|
236
|
+
end
|
237
|
+
```
|
238
|
+
|
239
|
+
`# box_component.html.erb`
|
240
|
+
```erb
|
241
|
+
<div class="Box">
|
242
|
+
<% if header %>
|
243
|
+
<div class="<%= header.class_names %>">
|
244
|
+
<%= header.content %>
|
245
|
+
</div>
|
246
|
+
<% end %>
|
247
|
+
<% if body %>
|
248
|
+
<div class="Box-body">
|
249
|
+
<%= body.content %>
|
250
|
+
</div>
|
251
|
+
<% end %>
|
252
|
+
<% if rows.any? %>
|
253
|
+
<ul>
|
254
|
+
<% rows.each do |row| %>
|
255
|
+
<li class="Box-row <%= row.theme_class_name %>">
|
256
|
+
<%= row.content %>
|
257
|
+
</li>
|
258
|
+
<% end %>
|
259
|
+
</ul>
|
260
|
+
<% end %>
|
261
|
+
<% if footer %>
|
262
|
+
<div class="Box-footer">
|
263
|
+
<%= footer %>
|
264
|
+
</div>
|
265
|
+
<% end %>
|
266
|
+
</div>
|
267
|
+
```
|
268
|
+
|
269
|
+
`# index.html.erb`
|
270
|
+
```erb
|
271
|
+
<%= render(BoxComponent.new) do |component| %>
|
272
|
+
<% component.slot(:header, class_names: "my-class-name") do %>
|
273
|
+
This is my header!
|
274
|
+
<% end %>
|
275
|
+
<% component.slot(:body) do %>
|
276
|
+
This is the body.
|
277
|
+
<% end %>
|
278
|
+
<% component.slot(:row) do %>
|
279
|
+
Row one
|
280
|
+
<% end %>
|
281
|
+
<% component.slot(:row, theme: :yellow) do %>
|
282
|
+
Yellow row
|
283
|
+
<% end %>
|
284
|
+
<% component.slot(:footer) do %>
|
285
|
+
This is the footer.
|
286
|
+
<% end %>
|
287
|
+
<% end %>
|
288
|
+
```
|
289
|
+
|
163
290
|
### Inline Component
|
164
291
|
|
165
292
|
ViewComponents can render without a template file, by defining a `call` method:
|
@@ -731,7 +858,8 @@ ViewComponent is far from a novel idea! Popular implementations of view componen
|
|
731
858
|
## Resources
|
732
859
|
|
733
860
|
- [Encapsulating Views, RailsConf 2020](https://youtu.be/YVYRus_2KZM)
|
734
|
-
- [Rethinking the View Layer with Components
|
861
|
+
- [Rethinking the View Layer with Components, Ruby Rogues Podcast](https://devchat.tv/ruby-rogues/rr-461-rethinking-the-view-layer-with-components-with-joel-hawksley/)
|
862
|
+
- [ViewComponents in Action with Andrew Mason, Ruby on Rails Podcast](https://5by5.tv/rubyonrails/320)
|
735
863
|
- [ViewComponent at GitHub with Joel Hawksley](https://the-ruby-blend.fireside.fm/9)
|
736
864
|
- [Components, HAML vs ERB, and Design Systems](https://the-ruby-blend.fireside.fm/4)
|
737
865
|
- [Choosing the Right Tech Stack with Dave Paola](https://5by5.tv/rubyonrails/307)
|
@@ -784,10 +912,10 @@ ViewComponent is built by:
|
|
784
912
|
|@simonrand|@fugufish|@cover|@franks921|@fsateler|
|
785
913
|
|Dublin, Ireland|Salt Lake City, Utah|Barcelona|South Africa|Chile|
|
786
914
|
|
787
|
-
|<img src="https://avatars.githubusercontent.com/maxbeizer?s=256" alt="maxbeizer" width="128" />|<img src="https://avatars.githubusercontent.com/franco?s=256" alt="franco" width="128" />|<img src="https://avatars.githubusercontent.com/tbroad-ramsey?s=256" alt="tbroad-ramsey" width="128" />|
|
788
|
-
|
789
|
-
|@maxbeizer|@franco|@tbroad-ramsey|
|
790
|
-
|Nashville, TN|Switzerland|Spring Hill, TN|
|
915
|
+
|<img src="https://avatars.githubusercontent.com/maxbeizer?s=256" alt="maxbeizer" width="128" />|<img src="https://avatars.githubusercontent.com/franco?s=256" alt="franco" width="128" />|<img src="https://avatars.githubusercontent.com/tbroad-ramsey?s=256" alt="tbroad-ramsey" width="128" />|<img src="https://avatars.githubusercontent.com/jensljungblad?s=256" alt="jensljungblad" width="128" />|<img src="https://avatars.githubusercontent.com/bbugh?s=256" alt="bbugh" width="128" />|
|
916
|
+
|:---:|:---:|:---:|:---:|:---:|
|
917
|
+
|@maxbeizer|@franco|@tbroad-ramsey|@jensljungblad|@bbugh|
|
918
|
+
|Nashville, TN|Switzerland|Spring Hill, TN|New York, NY|Austin, TX|
|
791
919
|
|
792
920
|
## License
|
793
921
|
|
data/lib/view_component/base.rb
CHANGED
@@ -5,6 +5,7 @@ require "active_support/configurable"
|
|
5
5
|
require "view_component/collection"
|
6
6
|
require "view_component/compile_cache"
|
7
7
|
require "view_component/previewable"
|
8
|
+
require "view_component/slotable"
|
8
9
|
|
9
10
|
module ViewComponent
|
10
11
|
class Base < ActionView::Base
|
@@ -17,6 +18,10 @@ module ViewComponent
|
|
17
18
|
class_attribute :content_areas
|
18
19
|
self.content_areas = [] # class_attribute:default doesn't work until Rails 5.2
|
19
20
|
|
21
|
+
# Hash of registered Slots
|
22
|
+
class_attribute :slots
|
23
|
+
self.slots = {}
|
24
|
+
|
20
25
|
# Entrypoint for rendering components.
|
21
26
|
#
|
22
27
|
# view_context: ActionView context from calling view
|
@@ -181,6 +186,10 @@ module ViewComponent
|
|
181
186
|
# has been re-defined by the consuming application, likely in ApplicationComponent.
|
182
187
|
child.source_location = caller_locations(1, 10).reject { |l| l.label == "inherited" }[0].absolute_path
|
183
188
|
|
189
|
+
# Clone slot configuration into child class
|
190
|
+
# see #test_slots_pollution
|
191
|
+
child.slots = self.slots.clone
|
192
|
+
|
184
193
|
super
|
185
194
|
end
|
186
195
|
|
@@ -0,0 +1,121 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/concern"
|
4
|
+
|
5
|
+
require "view_component/slot"
|
6
|
+
|
7
|
+
module ViewComponent
|
8
|
+
module Slotable
|
9
|
+
extend ActiveSupport::Concern
|
10
|
+
|
11
|
+
class_methods do
|
12
|
+
# support initializing slots as:
|
13
|
+
#
|
14
|
+
# with_slot(
|
15
|
+
# :header,
|
16
|
+
# collection: true|false,
|
17
|
+
# class_name: "Header" # class name string, used to instantiate Slot
|
18
|
+
# )
|
19
|
+
def with_slot(*slot_names, collection: false, class_name: nil)
|
20
|
+
slot_names.each do |slot_name|
|
21
|
+
# Ensure slot_name is not already declared
|
22
|
+
if self.slots.key?(slot_name)
|
23
|
+
raise ArgumentError.new("#{slot_name} slot declared multiple times")
|
24
|
+
end
|
25
|
+
|
26
|
+
# Ensure slot name is not :content
|
27
|
+
if slot_name == :content
|
28
|
+
raise ArgumentError.new ":content is a reserved slot name. Please use another name, such as ':body'"
|
29
|
+
end
|
30
|
+
|
31
|
+
# Set the name of the method used to access the Slot(s)
|
32
|
+
accessor_name =
|
33
|
+
if collection
|
34
|
+
# If Slot is a collection, set the accessor
|
35
|
+
# name to the pluralized form of the slot name
|
36
|
+
# For example: :tab => :tabs
|
37
|
+
ActiveSupport::Inflector.pluralize(slot_name)
|
38
|
+
else
|
39
|
+
slot_name
|
40
|
+
end
|
41
|
+
|
42
|
+
instance_variable_name = "@#{accessor_name}"
|
43
|
+
|
44
|
+
# If the slot is a collection, define an accesor that defaults to an empty array
|
45
|
+
if collection
|
46
|
+
class_eval <<-RUBY
|
47
|
+
def #{accessor_name}
|
48
|
+
#{instance_variable_name} ||= []
|
49
|
+
end
|
50
|
+
RUBY
|
51
|
+
else
|
52
|
+
attr_reader accessor_name
|
53
|
+
end
|
54
|
+
|
55
|
+
# Default class_name to ViewComponent::Slot
|
56
|
+
class_name = "ViewComponent::Slot" unless class_name.present?
|
57
|
+
|
58
|
+
# Register the slot on the component
|
59
|
+
self.slots[slot_name] = {
|
60
|
+
class_name: class_name,
|
61
|
+
instance_variable_name: instance_variable_name,
|
62
|
+
collection: collection
|
63
|
+
}
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# Build a Slot instance on a component,
|
69
|
+
# exposing it for use inside the
|
70
|
+
# component template.
|
71
|
+
#
|
72
|
+
# slot: Name of Slot, in symbol form
|
73
|
+
# **args: Arguments to be passed to Slot initializer
|
74
|
+
#
|
75
|
+
# For example:
|
76
|
+
# <%= render(SlotsComponent.new) do |component| %>
|
77
|
+
# <% component.slot(:footer, class_names: "footer-class") do %>
|
78
|
+
# <p>This is my footer!</p>
|
79
|
+
# <% end %>
|
80
|
+
# <% end %>
|
81
|
+
#
|
82
|
+
def slot(slot_name, **args, &block)
|
83
|
+
# Raise ArgumentError if `slot` does not exist
|
84
|
+
unless slots.keys.include?(slot_name)
|
85
|
+
raise ArgumentError.new "Unknown slot '#{slot_name}' - expected one of '#{slots.keys}'"
|
86
|
+
end
|
87
|
+
|
88
|
+
slot = slots[slot_name]
|
89
|
+
|
90
|
+
# The class name of the Slot, such as Header
|
91
|
+
slot_class = self.class.const_get(slot[:class_name])
|
92
|
+
|
93
|
+
unless slot_class <= ViewComponent::Slot
|
94
|
+
raise ArgumentError.new "#{slot[:class_name]} must inherit from ViewComponent::Slot"
|
95
|
+
end
|
96
|
+
|
97
|
+
# Instantiate Slot class, accommodating Slots that don't accept arguments
|
98
|
+
slot_instance = args.present? ? slot_class.new(args) : slot_class.new
|
99
|
+
|
100
|
+
# Capture block and assign to slot_instance#content
|
101
|
+
slot_instance.content = view_context.capture(&block) if block_given?
|
102
|
+
|
103
|
+
if slot[:collection]
|
104
|
+
# Initialize instance variable as an empty array
|
105
|
+
# if slot is a collection and has yet to be initialized
|
106
|
+
unless instance_variable_defined?(slot[:instance_variable_name])
|
107
|
+
instance_variable_set(slot[:instance_variable_name], [])
|
108
|
+
end
|
109
|
+
|
110
|
+
# Append Slot instance to collection accessor Array
|
111
|
+
instance_variable_get(slot[:instance_variable_name]) << slot_instance
|
112
|
+
else
|
113
|
+
# Assign the Slot instance to the slot accessor
|
114
|
+
instance_variable_set(slot[:instance_variable_name], slot_instance)
|
115
|
+
end
|
116
|
+
|
117
|
+
# Return nil, as this method should not output anything to the view itself.
|
118
|
+
nil
|
119
|
+
end
|
120
|
+
end
|
121
|
+
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.12.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-06-
|
11
|
+
date: 2020-06-30 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -207,6 +207,8 @@ files:
|
|
207
207
|
- lib/view_component/render_monkey_patch.rb
|
208
208
|
- lib/view_component/render_to_string_monkey_patch.rb
|
209
209
|
- lib/view_component/rendering_monkey_patch.rb
|
210
|
+
- lib/view_component/slot.rb
|
211
|
+
- lib/view_component/slotable.rb
|
210
212
|
- lib/view_component/template_error.rb
|
211
213
|
- lib/view_component/test_case.rb
|
212
214
|
- lib/view_component/test_helpers.rb
|