view_component 2.8.0 → 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 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