view_component 2.8.0 → 2.12.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7ac6dfbc2cb2fb7bbad0aac55de41cfbdc782dac0eb14f1e4b18784e15db02a9
4
- data.tar.gz: 294bd6faac8d95b0db842ab67139e141117058148c24d503647960e31c19ef21
3
+ metadata.gz: 8661a26312dc3649e30ee17fbc94f6fd73be410067f8a7fac8b6aaefc4338d11
4
+ data.tar.gz: 1024034624c49bb65bfe1fc3466886ffa60cd9699a58c7427ea6dbedb9baa03b
5
5
  SHA512:
6
- metadata.gz: 4d2da7f6d4ff599ea9b92408dc632eecb7629d7bf7cda9484eab20b0f02dd1a78ba3669fc5f9107f2b6388d2f5634258a14b806d58327c716a8339b819ef8aa7
7
- data.tar.gz: 50b86ff9e697608ae063352c8a55430763b5762016c3512dfa103f5df7a2fbb4875a993e319783f2e1e077c4ca157152a4314d3a106b0fc0a84186926044f917
6
+ metadata.gz: 24ddc5373c2fbb7cd81ef4f4077ea66a30ea797fbb9d4c444135d3d8590b5151596fddc18f5bc5e1f18fec65ca87572858513409385489524c11c1929cff3567
7
+ data.tar.gz: cf63eb464e88e02310cb55f9c6c779c31622174777dbd1f39a24d0e9ac40407c555ac51012c5ea8c06bdeccac7aab880475c02c678c3fbff9ef22f5ca6cf797e
@@ -1,5 +1,39 @@
1
1
  # master
2
2
 
3
+ # 2.12.0
4
+
5
+ * Implement Slots as potential successor to Content Areas.
6
+
7
+ *Jens Ljungblad, Brian Bugh, Jon Palmer, Joel Hawksley*
8
+
9
+ # 2.11.1
10
+
11
+ * Fix kwarg warnings in Ruby 2.7.
12
+
13
+ *Joel Hawksley*
14
+
15
+ # 2.11.0
16
+
17
+ * Ensure Rails configuration is available within components.
18
+
19
+ *Trevor Broaddus*
20
+
21
+ * Fix bug where global Rails helpers are inaccessible from nested components. Before, `helpers` was pointing to parent component.
22
+
23
+ *Franco Sebregondi*
24
+
25
+ # 2.10.0
26
+
27
+ * Raise an `ArgumentError` with a helpful message when Ruby cannot parse a component class.
28
+
29
+ *Max Beizer*
30
+
31
+ # 2.9.0
32
+
33
+ * Cache components per-request in development, preventing unnecessary recompilation during a single request.
34
+
35
+ *Felipe Sateler*
36
+
3
37
  # 2.8.0
4
38
 
5
39
  * Add `before_render`, deprecating `before_render_check`.
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,6 +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)
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)
734
863
  - [ViewComponent at GitHub with Joel Hawksley](https://the-ruby-blend.fireside.fm/9)
735
864
  - [Components, HAML vs ERB, and Design Systems](https://the-ruby-blend.fireside.fm/4)
736
865
  - [Choosing the Right Tech Stack with Dave Paola](https://5by5.tv/rubyonrails/307)
@@ -778,10 +907,15 @@ ViewComponent is built by:
778
907
  |@blakewilliams|@seanpdoyle|@tclem|@nashby|@jaredcwhite|
779
908
  |Boston, MA|New York, NY|San Francisco, CA|Minsk|Portland, OR|
780
909
 
781
- |<img src="https://avatars.githubusercontent.com/simonrand?s=256" alt="simonrand" width="128" />|<img src="https://avatars.githubusercontent.com/fugufish?s=256" alt="fugufish" width="128" />|<img src="https://avatars.githubusercontent.com/cover?s=256" alt="cover" width="128" />|<img src="https://avatars.githubusercontent.com/franks921?s=256" alt="franks921" width="128" />|
782
- |:---:|:---:|:---:|:---:|
783
- |@simonrand|@fugufish|@cover|@franks921|
784
- |Dublin, Ireland|Salt Lake City, Utah|Barcelona|South Africa|
910
+ |<img src="https://avatars.githubusercontent.com/simonrand?s=256" alt="simonrand" width="128" />|<img src="https://avatars.githubusercontent.com/fugufish?s=256" alt="fugufish" width="128" />|<img src="https://avatars.githubusercontent.com/cover?s=256" alt="cover" width="128" />|<img src="https://avatars.githubusercontent.com/franks921?s=256" alt="franks921" width="128" />|<img src="https://avatars.githubusercontent.com/fsateler?s=256" alt="fsateler" width="128" />|
911
+ |:---:|:---:|:---:|:---:|:---:|
912
+ |@simonrand|@fugufish|@cover|@franks921|@fsateler|
913
+ |Dublin, Ireland|Salt Lake City, Utah|Barcelona|South Africa|Chile|
914
+
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|
785
919
 
786
920
  ## License
787
921
 
@@ -3,7 +3,9 @@
3
3
  require "action_view"
4
4
  require "active_support/configurable"
5
5
  require "view_component/collection"
6
+ require "view_component/compile_cache"
6
7
  require "view_component/previewable"
8
+ require "view_component/slotable"
7
9
 
8
10
  module ViewComponent
9
11
  class Base < ActionView::Base
@@ -11,11 +13,15 @@ module ViewComponent
11
13
  include ViewComponent::Previewable
12
14
 
13
15
  # For CSRF authenticity tokens in forms
14
- delegate :form_authenticity_token, :protect_against_forgery?, to: :helpers
16
+ delegate :form_authenticity_token, :protect_against_forgery?, :config, to: :helpers
15
17
 
16
18
  class_attribute :content_areas
17
19
  self.content_areas = [] # class_attribute:default doesn't work until Rails 5.2
18
20
 
21
+ # Hash of registered Slots
22
+ class_attribute :slots
23
+ self.slots = {}
24
+
19
25
  # Entrypoint for rendering components.
20
26
  #
21
27
  # view_context: ActionView context from calling view
@@ -105,9 +111,9 @@ module ViewComponent
105
111
  @controller ||= view_context.controller
106
112
  end
107
113
 
108
- # Provides a proxy to access helper methods
114
+ # Provides a proxy to access helper methods from the context of the current controller
109
115
  def helpers
110
- @helpers ||= view_context
116
+ @helpers ||= controller.view_context
111
117
  end
112
118
 
113
119
  # Removes the first part of the path and the extension.
@@ -160,8 +166,8 @@ module ViewComponent
160
166
  attr_accessor :source_location
161
167
 
162
168
  # Render a component collection.
163
- def with_collection(*args)
164
- Collection.new(self, *args)
169
+ def with_collection(collection, **args)
170
+ Collection.new(self, collection, **args)
165
171
  end
166
172
 
167
173
  # Provide identifier for ActionView template annotations
@@ -180,6 +186,10 @@ module ViewComponent
180
186
  # has been re-defined by the consuming application, likely in ApplicationComponent.
181
187
  child.source_location = caller_locations(1, 10).reject { |l| l.label == "inherited" }[0].absolute_path
182
188
 
189
+ # Clone slot configuration into child class
190
+ # see #test_slots_pollution
191
+ child.slots = self.slots.clone
192
+
183
193
  super
184
194
  end
185
195
 
@@ -192,9 +202,7 @@ module ViewComponent
192
202
  end
193
203
 
194
204
  def compiled?
195
- @compiled ||= false
196
-
197
- @compiled && ActionView::Base.cache_template_loading
205
+ CompileCache.compiled?(self)
198
206
  end
199
207
 
200
208
  # Compile templates to instance methods, assuming they haven't been compiled already.
@@ -270,7 +278,7 @@ module ViewComponent
270
278
  RUBY
271
279
  end
272
280
 
273
- @compiled = true
281
+ CompileCache.register self
274
282
  end
275
283
 
276
284
  # we'll eventually want to update this to support other types
@@ -308,7 +316,16 @@ module ViewComponent
308
316
  parameter = validate_default ? collection_parameter : provided_collection_parameter
309
317
 
310
318
  return unless parameter
311
- return if instance_method(:initialize).parameters.map(&:last).include?(parameter)
319
+ return if initialize_parameters.map(&:last).include?(parameter)
320
+
321
+ # If Ruby cannot parse the component class, then the initalize
322
+ # parameters will be empty and ViewComponent will not be able to render
323
+ # the component.
324
+ if initialize_parameters.empty?
325
+ raise ArgumentError.new(
326
+ "#{self} initializer is empty or invalid."
327
+ )
328
+ end
312
329
 
313
330
  raise ArgumentError.new(
314
331
  "#{self} initializer must accept " \
@@ -318,6 +335,10 @@ module ViewComponent
318
335
 
319
336
  private
320
337
 
338
+ def initialize_parameters
339
+ instance_method(:initialize).parameters
340
+ end
341
+
321
342
  def provided_collection_parameter
322
343
  @provided_collection_parameter ||= nil
323
344
  end
@@ -9,7 +9,7 @@ module ViewComponent
9
9
  @component.validate_collection_parameter!(validate_default: true)
10
10
 
11
11
  @collection.map do |item|
12
- content = @component.new(component_options(item, iterator)).render_in(view_context, &block)
12
+ content = @component.new(**component_options(item, iterator)).render_in(view_context, &block)
13
13
  iterator.iterate!
14
14
  content
15
15
  end.join.html_safe
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ViewComponent
4
+ # Keeps track of which templates have already been compiled
5
+ # This is not part of the public API
6
+ module CompileCache
7
+ mattr_accessor :cache, instance_reader: false, instance_accessor: false do
8
+ Set.new
9
+ end
10
+ module_function
11
+
12
+ def register(klass)
13
+ cache << klass
14
+ end
15
+
16
+ def compiled?(klass)
17
+ cache.include? klass
18
+ end
19
+
20
+ def invalidate!
21
+ cache.clear
22
+ end
23
+ end
24
+ end
@@ -69,6 +69,10 @@ module ViewComponent
69
69
  get "#{options.preview_route}/*path", to: "view_components#previews", as: :preview_view_component, internal: true
70
70
  end
71
71
  end
72
+
73
+ app.executor.to_run :before do
74
+ CompileCache.invalidate! unless ActionView::Base.cache_template_loading
75
+ end
72
76
  end
73
77
  end
74
78
  end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ViewComponent
4
+ class Slot
5
+ attr_accessor :content
6
+ end
7
+ end
@@ -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
@@ -3,7 +3,7 @@
3
3
  module ViewComponent
4
4
  module VERSION
5
5
  MAJOR = 2
6
- MINOR = 8
6
+ MINOR = 12
7
7
  PATCH = 0
8
8
 
9
9
  STRING = [MAJOR, MINOR, PATCH].join(".")
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.8.0
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-08 00:00:00.000000000 Z
11
+ date: 2020-06-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -200,12 +200,15 @@ files:
200
200
  - lib/view_component.rb
201
201
  - lib/view_component/base.rb
202
202
  - lib/view_component/collection.rb
203
+ - lib/view_component/compile_cache.rb
203
204
  - lib/view_component/engine.rb
204
205
  - lib/view_component/preview.rb
205
206
  - lib/view_component/previewable.rb
206
207
  - lib/view_component/render_monkey_patch.rb
207
208
  - lib/view_component/render_to_string_monkey_patch.rb
208
209
  - lib/view_component/rendering_monkey_patch.rb
210
+ - lib/view_component/slot.rb
211
+ - lib/view_component/slotable.rb
209
212
  - lib/view_component/template_error.rb
210
213
  - lib/view_component/test_case.rb
211
214
  - lib/view_component/test_helpers.rb