view_component 2.16.0 → 2.18.2

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: 9ccdcdb195fbc49953c3663294a7345eb012f88344d1375456a07c6ef781b1d8
4
- data.tar.gz: b2cf7cb1139e318f2ca7712242e610c31a42c78448a5d3151db1550a41e00355
3
+ metadata.gz: a2198b33fb53185423a45c6f13023650604e7dacc2f5b44c2455f44b684ffdbc
4
+ data.tar.gz: f9eaffd030eb7bef12b243cfdf42333e4f5b28bf6a975f6b4720b65ef0d6aedc
5
5
  SHA512:
6
- metadata.gz: 72657816a14d62166791f37d2f83cfd81efe8f186678bcf21e40c50d38d64663fbc4e2b9fe881820ba26f2a7ea258d5d6c0cb47a73132d5443f66db3489d1921
7
- data.tar.gz: e7abe1071862dfb3208e2615176fd352b0f6cd6e7ef947b1dfb4528e435682908cd01ab8091b33612b234eebf7121a7dd00e5cb8890bc6ae1160fdf5bcbdcd3f
6
+ metadata.gz: 9ab6f420e32b85e912b4407ab3679e3fd696800547aecaa13c15c7833c82564f7ba8bc7db4fad8dbd7aa201250db156901486ee3c12b13f37ffedca3e5cbacf1
7
+ data.tar.gz: 22103cb47921ba57712808c6a3130f48926a40179a3564fab9b2235dc892edde3ed81df28e6c23535685d13121b4be1f232836612e7132d4be04143595eebb6d
@@ -1,5 +1,47 @@
1
1
  # master
2
2
 
3
+ # 2.18.2
4
+
5
+ * Raise an error if controller or view context is accessed during initialize as they are only available in render.
6
+
7
+ *Julian Nadeau*
8
+
9
+ * Collate test coverage across CI builds, ensuring 100% test coverage.
10
+
11
+ *Joel Hawksley*
12
+
13
+ # 2.18.1
14
+
15
+ * Fix bug where previews didn't work when monkey patch was disabled.
16
+
17
+ *Mixer Gutierrez*
18
+
19
+ # 2.18.0
20
+
21
+ * Fix auto-loading of previews (changes no longer require a server restart)
22
+
23
+ *Matt Brictson*
24
+
25
+ * Add `default_preview_layout` configuration option to load custom app/views/layouts.
26
+
27
+ *Jared White, Juan Manuel Ramallo*
28
+
29
+ * Calculate virtual_path once for all instances of a component class to improve performance.
30
+
31
+ *Brad Parker*
32
+
33
+ # 2.17.1
34
+
35
+ * Fix bug where rendering Slot with empty block resulted in error.
36
+
37
+ *Joel Hawksley*
38
+
39
+ # 2.17.0
40
+
41
+ * Slots return stripped HTML, removing leading and trailing whitespace.
42
+
43
+ *Jason Long, Joel Hawksley*
44
+
3
45
  # 2.16.0
4
46
 
5
47
  * Add `--sidecar` option to the erb, haml and slim generators. Places the generated template in the sidecar directory.
data/README.md CHANGED
@@ -47,6 +47,10 @@ Traditional Rails views have an implicit interface, making it hard to reason abo
47
47
 
48
48
  ViewComponents use a standard Ruby initializer that clearly defines what is needed to render, making them easier (and safer) to reuse than partials.
49
49
 
50
+ #### Performance
51
+
52
+ Based on our [benchmarks](performance/benchmark.rb), ViewComponents are ~10x faster than partials.
53
+
50
54
  #### Standards
51
55
 
52
56
  Views often fail basic Ruby code quality standards: long methods, deep conditional nesting, and mystery guests abound.
@@ -164,19 +168,17 @@ Returning:
164
168
 
165
169
  _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
170
 
167
- Slots enable multiple blocks of content to be passed to a single ViewComponent.
171
+ Slots enable multiple blocks of content to be passed to a single ViewComponent, reducing the need for sub-components (e.g. ModalHeader, ModalBody).
168
172
 
169
- Slots exist in two forms: normal slots and collection slots.
173
+ By default, slots can be rendered once per component. They provide an accessor with the name of the slot (`#header`) that returns an instance of `ViewComponent::Slot`, etc.
170
174
 
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.
175
+ Slots declared with `collection: true` can be rendered multiple times. They provide an accessor with the pluralized name of the slot (`#rows`), which is an Array of `ViewComponent::Slot` instances.
172
176
 
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.
177
+ To learn more about the design of the Slots API, see https://github.com/github/view_component/pull/348 and https://github.com/github/view_component/discussions/325.
176
178
 
177
179
  ##### Defining Slots
178
180
 
179
- Slots are defined by the `with_slot` macro:
181
+ Slots are defined by `with_slot`:
180
182
 
181
183
  `with_slot :header`
182
184
 
@@ -184,11 +186,11 @@ To define a collection slot, add `collection: true`:
184
186
 
185
187
  `with_slot :row, collection: true`
186
188
 
187
- To define a slot with a custom class, pass `class_name`:
189
+ To define a slot with a custom Ruby class, pass `class_name`:
188
190
 
189
191
  `with_slot :body, class_name: 'BodySlot`
190
192
 
191
- Slot classes should be subclasses of `ViewComponent::Slot`.
193
+ _Note: Slot classes must be subclasses of `ViewComponent::Slot`._
192
194
 
193
195
  ##### Example ViewComponent with Slots
194
196
 
@@ -202,12 +204,12 @@ class BoxComponent < ViewComponent::Base
202
204
  with_slot :row, collection: true, class_name: "Row"
203
205
 
204
206
  class Header < ViewComponent::Slot
205
- def initialize(class_names: "")
206
- @class_names = class_names
207
+ def initialize(classes: "")
208
+ @classes = classes
207
209
  end
208
210
 
209
- def class_names
210
- "Box-header #{@class_names}"
211
+ def classes
212
+ "Box-header #{@classes}"
211
213
  end
212
214
  end
213
215
 
@@ -240,7 +242,7 @@ end
240
242
  ```erb
241
243
  <div class="Box">
242
244
  <% if header %>
243
- <div class="<%= header.class_names %>">
245
+ <div class="<%= header.classes %>">
244
246
  <%= header.content %>
245
247
  </div>
246
248
  <% end %>
@@ -260,7 +262,7 @@ end
260
262
  <% end %>
261
263
  <% if footer %>
262
264
  <div class="Box-footer">
263
- <%= footer %>
265
+ <%= footer.content %>
264
266
  </div>
265
267
  <% end %>
266
268
  </div>
@@ -269,7 +271,7 @@ end
269
271
  `# index.html.erb`
270
272
  ```erb
271
273
  <%= render(BoxComponent.new) do |component| %>
272
- <% component.slot(:header, class_names: "my-class-name") do %>
274
+ <% component.slot(:header, classes: "my-class-name") do %>
273
275
  This is my header!
274
276
  <% end %>
275
277
  <% component.slot(:body) do %>
@@ -360,6 +362,32 @@ bin/rails generate component Example title content --sidecar
360
362
  create app/components/example_component/example_component.html.erb
361
363
  ```
362
364
 
365
+ #### Component file inside Sidecar directory
366
+
367
+ It's also possible to place the Ruby component file inside the sidecar directory, grouping all related files in the same folder:
368
+
369
+ _Note: Avoid giving your containing folder the same name as your `.rb` file or there will be a conflict between Module and Class definitions_
370
+
371
+ ```
372
+ app/components
373
+ ├── ...
374
+ ├── example
375
+ | ├── component.rb
376
+ | ├── component.css
377
+ | ├── component.html.erb
378
+ | └── component.js
379
+ ├── ...
380
+
381
+ ```
382
+
383
+ The component can then be rendered using the folder name as a namespace:
384
+
385
+ ```erb
386
+ <%= render(Example::Component.new(title: "my title")) do %>
387
+ Hello, World!
388
+ <% end %>
389
+ ```
390
+
363
391
  ### Conditional Rendering
364
392
 
365
393
  Components can implement a `#render?` method to be called after initialization to determine if the component should render.
@@ -674,6 +702,14 @@ class TestComponentPreview < ViewComponent::Preview
674
702
  end
675
703
  ```
676
704
 
705
+ You can also set a custom layout to be used by default for previews as well as the preview index pages via the `default_preview_layout` configuration option:
706
+
707
+ `config/application.rb`
708
+ ```ruby
709
+ # Set the default layout to app/views/layouts/component_preview.html.erb
710
+ config.view_component.default_preview_layout = "component_preview"
711
+ ```
712
+
677
713
  Preview classes live in `test/components/previews`, which can be configured using the `preview_paths` option:
678
714
 
679
715
  `config/application.rb`
@@ -990,10 +1026,15 @@ ViewComponent is built by:
990
1026
  |@maxbeizer|@franco|@tbroad-ramsey|@jensljungblad|@bbugh|
991
1027
  |Nashville, TN|Switzerland|Spring Hill, TN|New York, NY|Austin, TX|
992
1028
 
993
- |<img src="https://avatars.githubusercontent.com/johannesengl?s=256" alt="johannesengl" width="128" />|<img src="https://avatars.githubusercontent.com/czj?s=256" alt="czj" width="128" />|<img src="https://avatars.githubusercontent.com/mrrooijen?s=256" alt="mrrooijen" width="128" />|
994
- |:---:|:---:|:---:|
995
- |@johannesengl|@czj|@mrrooijen|
996
- |Berlin, Germany|Paris, France|The Netherlands|
1029
+ |<img src="https://avatars.githubusercontent.com/johannesengl?s=256" alt="johannesengl" width="128" />|<img src="https://avatars.githubusercontent.com/czj?s=256" alt="czj" width="128" />|<img src="https://avatars.githubusercontent.com/mrrooijen?s=256" alt="mrrooijen" width="128" />|<img src="https://avatars.githubusercontent.com/bradparker?s=256" alt="bradparker" width="128" />|<img src="https://avatars.githubusercontent.com/mattbrictson?s=256" alt="mattbrictson" width="128" />|
1030
+ |:---:|:---:|:---:|:---:|:---:|
1031
+ |@johannesengl|@czj|@mrrooijen|@bradparker|@mattbrictson|
1032
+ |Berlin, Germany|Paris, France|The Netherlands|Brisbane, Australia|San Francisco|
1033
+
1034
+ |<img src="https://avatars.githubusercontent.com/mixergtz?s=256" alt="mixergtz" width="128" />|<img src="https://avatars.githubusercontent.com/jules2689?s=256" alt="jules2689" width="128" />|
1035
+ |:---:|:---:|
1036
+ |@mixergtz|@jules2689|
1037
+ |Medellin, Colombia|Toronto, Canada|
997
1038
 
998
1039
  ## License
999
1040
 
@@ -16,22 +16,23 @@ class ViewComponentsController < Rails::ApplicationController # :nodoc:
16
16
  def index
17
17
  @previews = ViewComponent::Preview.all
18
18
  @page_title = "Component Previews"
19
+ render "view_components/index", **determine_layout
19
20
  end
20
21
 
21
22
  def previews
22
23
  if params[:path] == @preview.preview_name
23
24
  @page_title = "Component Previews for #{@preview.preview_name}"
24
- render "view_components/previews"
25
+ render "view_components/previews", **determine_layout
25
26
  else
26
27
  prepend_application_view_paths
27
28
  prepend_preview_examples_view_path
28
29
  @example_name = File.basename(params[:path])
29
30
  @render_args = @preview.render_args(@example_name, params: params.permit!)
30
- layout = @render_args[:layout]
31
+ layout = determine_layout(@render_args[:layout], prepend_views: false)[:layout]
31
32
  template = @render_args[:template]
32
33
  locals = @render_args[:locals]
33
34
  opts = {}
34
- opts[:layout] = layout if layout.present?
35
+ opts[:layout] = layout if layout.present? || layout == false
35
36
  opts[:locals] = locals if locals.present?
36
37
  render template, opts # rubocop:disable GitHub/RailsControllerRenderLiteral
37
38
  end
@@ -39,6 +40,10 @@ class ViewComponentsController < Rails::ApplicationController # :nodoc:
39
40
 
40
41
  private
41
42
 
43
+ def default_preview_layout # :doc:
44
+ ViewComponent::Base.default_preview_layout
45
+ end
46
+
42
47
  def show_previews? # :doc:
43
48
  ViewComponent::Base.show_previews
44
49
  end
@@ -61,6 +66,24 @@ class ViewComponentsController < Rails::ApplicationController # :nodoc:
61
66
  end
62
67
  end
63
68
 
69
+ # Returns either {} or {layout: value} depending on configuration
70
+ def determine_layout(layout_override = nil, prepend_views: true)
71
+ return {} unless defined?(Rails.root)
72
+
73
+ layout_declaration = {}
74
+
75
+ if !layout_override.nil?
76
+ # Allow component-level override, even if false (thus no layout rendered)
77
+ layout_declaration[:layout] = layout_override
78
+ elsif default_preview_layout.present?
79
+ layout_declaration[:layout] = default_preview_layout
80
+ end
81
+
82
+ prepend_application_view_paths if layout_declaration[:layout].present? && prepend_views
83
+
84
+ layout_declaration
85
+ end
86
+
64
87
  def prepend_application_view_paths
65
88
  prepend_view_path Rails.root.join("app/views") if defined?(Rails.root)
66
89
  end
@@ -1 +1,5 @@
1
- <%= render(@render_args[:component], @render_args[:args], &@render_args[:block])%>
1
+ <% if ViewComponent::Base.render_monkey_patch_enabled || Rails.version.to_f >= 6.1 %>
2
+ <%= render(@render_args[:component], @render_args[:args], &@render_args[:block])%>
3
+ <% else %>
4
+ <%= render_component(@render_args[:component], &@render_args[:block])%>
5
+ <% end %>
@@ -12,6 +12,8 @@ module ViewComponent
12
12
  include ActiveSupport::Configurable
13
13
  include ViewComponent::Previewable
14
14
 
15
+ ViewContextCalledBeforeRenderError = Class.new(StandardError)
16
+
15
17
  # For CSRF authenticity tokens in forms
16
18
  delegate :form_authenticity_token, :protect_against_forgery?, :config, to: :helpers
17
19
 
@@ -108,17 +110,19 @@ module ViewComponent
108
110
  end
109
111
 
110
112
  def controller
113
+ raise ViewContextCalledBeforeRenderError, "`controller` can only be called at render time." if view_context.nil?
111
114
  @controller ||= view_context.controller
112
115
  end
113
116
 
114
117
  # Provides a proxy to access helper methods from the context of the current controller
115
118
  def helpers
119
+ raise ViewContextCalledBeforeRenderError, "`helpers` can only be called at render time." if view_context.nil?
116
120
  @helpers ||= controller.view_context
117
121
  end
118
122
 
119
- # Removes the first part of the path and the extension.
123
+ # Exposes .virutal_path as an instance method
120
124
  def virtual_path
121
- self.class.source_location.gsub(%r{(.*app/components)|(\.rb)}, "")
125
+ self.class.virtual_path
122
126
  end
123
127
 
124
128
  # For caching, such as #cache_if
@@ -166,7 +170,7 @@ module ViewComponent
166
170
  mattr_accessor :render_monkey_patch_enabled, instance_writer: false, default: true
167
171
 
168
172
  class << self
169
- attr_accessor :source_location
173
+ attr_accessor :source_location, :virtual_path
170
174
 
171
175
  # Render a component collection.
172
176
  def with_collection(collection, **args)
@@ -189,6 +193,9 @@ module ViewComponent
189
193
  # has been re-defined by the consuming application, likely in ApplicationComponent.
190
194
  child.source_location = caller_locations(1, 10).reject { |l| l.label == "inherited" }[0].absolute_path
191
195
 
196
+ # Removes the first part of the path and the extension.
197
+ child.virtual_path = child.source_location.gsub(%r{(.*app/components)|(\.rb)}, "")
198
+
192
199
  # Clone slot configuration into child class
193
200
  # see #test_slots_pollution
194
201
  child.slots = self.slots.clone
@@ -378,17 +385,17 @@ module ViewComponent
378
385
 
379
386
  location_without_extension = source_location.chomp(File.extname(source_location))
380
387
 
381
- extenstions = ActionView::Template.template_handler_extensions.join(",")
388
+ extensions = ActionView::Template.template_handler_extensions.join(",")
382
389
 
383
- # view files in the same directory as te component
384
- sidecar_files = Dir["#{location_without_extension}.*{#{extenstions}}"]
390
+ # view files in the same directory as the component
391
+ sidecar_files = Dir["#{location_without_extension}.*{#{extensions}}"]
385
392
 
386
393
  # view files in a directory named like the component
387
394
  directory = File.dirname(source_location)
388
395
  filename = File.basename(source_location, ".rb")
389
396
  component_name = name.demodulize.underscore
390
397
 
391
- sidecar_directory_files = Dir["#{directory}/#{component_name}/#{filename}.*{#{extenstions}}"]
398
+ sidecar_directory_files = Dir["#{directory}/#{component_name}/#{filename}.*{#{extensions}}"]
392
399
 
393
400
  (sidecar_files - [source_location] + sidecar_directory_files)
394
401
  end
@@ -34,8 +34,8 @@ module ViewComponent
34
34
  initializer "view_component.set_autoload_paths" do |app|
35
35
  options = app.config.view_component
36
36
 
37
- if options.show_previews && options.preview_path
38
- ActiveSupport::Dependencies.autoload_paths << options.preview_path
37
+ if options.show_previews && !options.preview_paths.empty?
38
+ ActiveSupport::Dependencies.autoload_paths.concat(options.preview_paths)
39
39
  end
40
40
  end
41
41
 
@@ -24,6 +24,8 @@ module ViewComponent # :nodoc:
24
24
  }
25
25
  end
26
26
 
27
+ alias_method :render_component, :render
28
+
27
29
  class << self
28
30
  # Returns all component preview classes.
29
31
  def all
@@ -42,21 +44,11 @@ module ViewComponent # :nodoc:
42
44
  result.merge(layout: @layout)
43
45
  end
44
46
 
45
- # Returns the component object class associated to the preview.
46
- def component
47
- name.chomp("Preview").constantize
48
- end
49
-
50
47
  # Returns all of the available examples for the component preview.
51
48
  def examples
52
49
  public_instance_methods(false).map(&:to_s).sort
53
50
  end
54
51
 
55
- # Returns +true+ if the example of the component preview exists.
56
- def example_exists?(example)
57
- examples.include?(example)
58
- end
59
-
60
52
  # Returns +true+ if the preview exists.
61
53
  def exists?(preview)
62
54
  all.any? { |p| p.preview_name == preview }
@@ -102,10 +94,6 @@ module ViewComponent # :nodoc:
102
94
  def preview_paths
103
95
  Base.preview_paths
104
96
  end
105
-
106
- def show_previews
107
- Base.show_previews
108
- end
109
97
  end
110
98
  end
111
99
  end
@@ -7,6 +7,14 @@ module ViewComponent # :nodoc:
7
7
  extend ActiveSupport::Concern
8
8
 
9
9
  included do
10
+ # Set a custom default preview layout through app configuration:
11
+ #
12
+ # config.view_component.default_preview_layout = "component_preview"
13
+ #
14
+ # This affects preview index pages as well as individual component previews
15
+ #
16
+ mattr_accessor :default_preview_layout, instance_writer: false
17
+
10
18
  # Set the location of component previews through app configuration:
11
19
  #
12
20
  # config.view_component.preview_paths << "#{Rails.root}/lib/component_previews"
@@ -98,7 +98,7 @@ module ViewComponent
98
98
  slot_instance = args.present? ? slot_class.new(**args) : slot_class.new
99
99
 
100
100
  # Capture block and assign to slot_instance#content
101
- slot_instance.content = view_context.capture(&block) if block_given?
101
+ slot_instance.content = view_context.capture(&block).to_s.strip.html_safe if block_given?
102
102
 
103
103
  if slot[:collection]
104
104
  # Initialize instance variable as an empty array
@@ -14,7 +14,11 @@ module ViewComponent
14
14
  assert_no_selector("body")
15
15
  end
16
16
  rescue LoadError
17
+ # We don't have a test case for running an application without capybara installed.
18
+ # It's probably fine to leave this without coverage.
19
+ # :nocov:
17
20
  warn "WARNING in `ViewComponent::TestHelpers`: You must add `capybara` to your Gemfile to use Capybara assertions." if ENV["DEBUG"]
21
+ # :nocov:
18
22
  end
19
23
 
20
24
  attr_reader :rendered_component
@@ -3,8 +3,8 @@
3
3
  module ViewComponent
4
4
  module VERSION
5
5
  MAJOR = 2
6
- MINOR = 16
7
- PATCH = 0
6
+ MINOR = 18
7
+ PATCH = 2
8
8
 
9
9
  STRING = [MAJOR, MINOR, PATCH].join(".")
10
10
  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.16.0
4
+ version: 2.18.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - GitHub Open Source
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-07-15 00:00:00.000000000 Z
11
+ date: 2020-09-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -30,6 +30,20 @@ dependencies:
30
30
  - - "<"
31
31
  - !ruby/object:Gem::Version
32
32
  version: '7.0'
33
+ - !ruby/object:Gem::Dependency
34
+ name: benchmark-ips
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: 2.8.2
40
+ type: :development
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: 2.8.2
33
47
  - !ruby/object:Gem::Dependency
34
48
  name: bundler
35
49
  requirement: !ruby/object:Gem::Requirement
@@ -157,20 +171,20 @@ dependencies:
157
171
  - !ruby/object:Gem::Version
158
172
  version: 0.18.0
159
173
  - !ruby/object:Gem::Dependency
160
- name: simplecov-erb
174
+ name: simplecov-console
161
175
  requirement: !ruby/object:Gem::Requirement
162
176
  requirements:
163
177
  - - "~>"
164
178
  - !ruby/object:Gem::Version
165
- version: '0.1'
179
+ version: 0.7.2
166
180
  type: :development
167
181
  prerelease: false
168
182
  version_requirements: !ruby/object:Gem::Requirement
169
183
  requirements:
170
184
  - - "~>"
171
185
  - !ruby/object:Gem::Version
172
- version: '0.1'
173
- description:
186
+ version: 0.7.2
187
+ description:
174
188
  email:
175
189
  - opensource+view_component@github.com
176
190
  executables: []
@@ -222,7 +236,7 @@ licenses:
222
236
  - MIT
223
237
  metadata:
224
238
  allowed_push_host: https://rubygems.org
225
- post_install_message:
239
+ post_install_message:
226
240
  rdoc_options: []
227
241
  require_paths:
228
242
  - lib
@@ -238,7 +252,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
238
252
  version: '0'
239
253
  requirements: []
240
254
  rubygems_version: 3.1.2
241
- signing_key:
255
+ signing_key:
242
256
  specification_version: 4
243
257
  summary: View components for Rails
244
258
  test_files: []