view_component 2.57.1 → 2.59.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: 1c0dea17d60642f97d9edad061c538054015eac0f774bc58d37ba30c177a76ac
4
- data.tar.gz: bb12f7080b7c90ab0e475550bf44577ef25d2b1e0d0966cc5f07c272d9ea5fec
3
+ metadata.gz: ca82494216cd6b91d9a073bd1ab025cbe48049560893a500e72caff01141e02f
4
+ data.tar.gz: 9eecd79663f09edb31cae5a104b861c4922a99a92307f583b0703b11053a2d60
5
5
  SHA512:
6
- metadata.gz: 3eb53d5be8bdb0ab083f0c57938bda3df2094c6368619efb59254bd8ce6a749ffcd4d1dab653b80be48a17f361fa15611c9086c2a526891d81e92527ffd67c9d
7
- data.tar.gz: 418af4138439260637f53b025de631a11f222a699030afe36d3b51b7a31c5f451e7d3eb2ec5c6d13fa39b9b44e12e27fae972b631c92133bfbf054079695b80d
6
+ metadata.gz: 4e04336d18bd57be41e3ae4637ae1978a4884a352fced4b852a40901fa1ff02a13e6266ab4c669fbb3669fa718eecbfd8a5b13c54df79cbb9b692e278f02f35e
7
+ data.tar.gz: 2fed6153f148f683afe4cd27c909051aedfc92e9f896e6fe41736083d05217e7df15ac12799291fd2299ca59c1ee12e7ec1145ac9be5ee78cacb81d55fccf548
@@ -37,21 +37,24 @@ module ViewComponent
37
37
  opts = {}
38
38
  opts[:layout] = layout if layout.present? || layout == false
39
39
  opts[:locals] = locals if locals.present?
40
- render "view_components/preview", opts # rubocop:disable GitHub/RailsControllerRenderLiteral
40
+ render "view_components/preview", opts
41
41
  end
42
42
  end
43
43
 
44
44
  private
45
45
 
46
- def default_preview_layout # :doc:
46
+ # :doc:
47
+ def default_preview_layout
47
48
  ViewComponent::Base.default_preview_layout
48
49
  end
49
50
 
50
- def show_previews? # :doc:
51
+ # :doc:
52
+ def show_previews?
51
53
  ViewComponent::Base.show_previews
52
54
  end
53
55
 
54
- def find_preview # :doc:
56
+ # :doc:
57
+ def find_preview
55
58
  candidates = []
56
59
  params[:path].to_s.scan(%r{/|$}) { candidates << $` }
57
60
  preview = candidates.detect { |candidate| ViewComponent::Preview.exists?(candidate) }
@@ -7,14 +7,14 @@ module PreviewHelper
7
7
  def preview_source
8
8
  return if @render_args.nil?
9
9
 
10
- render "preview_source" # rubocop:disable GitHub/RailsViewRenderPathsExist
10
+ render "preview_source"
11
11
  end
12
12
 
13
13
  def find_template_data(lookup_context:, template_identifier:)
14
14
  template = lookup_context.find_template(template_identifier)
15
15
 
16
16
  if Rails.version.to_f >= 6.1 || template.source.present?
17
- return {
17
+ {
18
18
  source: template.source,
19
19
  prism_language_name: prism_language_name_by_template(template: template)
20
20
  }
@@ -40,7 +40,7 @@ module PreviewHelper
40
40
  template_source = File.read(template_file_path)
41
41
  prism_language_name = prism_language_name_by_template_path(template_file_path: template_file_path)
42
42
 
43
- return {
43
+ {
44
44
  source: template_source,
45
45
  prism_language_name: prism_language_name
46
46
  }
data/docs/CHANGELOG.md CHANGED
@@ -9,6 +9,102 @@ title: Changelog
9
9
 
10
10
  ## main
11
11
 
12
+ ## 2.59.0
13
+
14
+ * Expose Capybara DSL methods directly inside tests.
15
+
16
+ The following Capybara methods are now available directly without having to use the `page` method:
17
+
18
+ * [`all`](https://rubydoc.info/github/teamcapybara/capybara/Capybara%2FNode%2FFinders:all)
19
+ * [`first`](https://rubydoc.info/github/teamcapybara/capybara/Capybara%2FNode%2FFinders:first)
20
+ * [`text`](https://rubydoc.info/github/teamcapybara/capybara/Capybara%2FNode%2FSimple:text)
21
+ * [`find`](https://rubydoc.info/github/teamcapybara/capybara/master/Capybara%2FNode%2FFinders:find)
22
+ * [`find_all`](https://rubydoc.info/github/teamcapybara/capybara/master/Capybara%2FNode%2FFinders:find_all)
23
+ * [`find_button`](https://rubydoc.info/github/teamcapybara/capybara/master/Capybara%2FNode%2FFinders:find_button)
24
+ * [`find_by_id`](https://rubydoc.info/github/teamcapybara/capybara/master/Capybara%2FNode%2FFinders:find_by_id)
25
+ * [`find_field`](https://rubydoc.info/github/teamcapybara/capybara/master/Capybara%2FNode%2FFinders:find_field)
26
+ * [`find_link`](https://rubydoc.info/github/teamcapybara/capybara/master/Capybara%2FNode%2FFinders:find_link)
27
+ * [`has_content?`](https://rubydoc.info/github/teamcapybara/capybara/master/Capybara%2FNode%2FMatchers:has_content%3F)
28
+ * [`has_text?`](https://rubydoc.info/github/teamcapybara/capybara/master/Capybara%2FNode%2FMatchers:has_text%3F)
29
+ * [`has_css?`](https://rubydoc.info/github/teamcapybara/capybara/master/Capybara%2FNode%2FMatchers:has_css%3F)
30
+ * [`has_no_content?`](https://rubydoc.info/github/teamcapybara/capybara/master/Capybara%2FNode%2FMatchers:has_no_content%3F)
31
+ * [`has_no_text?`](https://rubydoc.info/github/teamcapybara/capybara/master/Capybara%2FNode%2FMatchers:has_no_text%3F)
32
+ * [`has_no_css?`](https://rubydoc.info/github/teamcapybara/capybara/master/Capybara%2FNode%2FMatchers:has_no_css%3F)
33
+ * [`has_no_xpath?`](https://rubydoc.info/github/teamcapybara/capybara/master/Capybara%2FNode%2FMatchers:has_no_xpath%3F)
34
+ * [`has_xpath?`](https://rubydoc.info/github/teamcapybara/capybara/master/Capybara%2FNode%2FMatchers:has_xpath%3F)
35
+ * [`has_link?`](https://rubydoc.info/github/teamcapybara/capybara/master/Capybara%2FNode%2FMatchers:has_link%3F)
36
+ * [`has_no_link?`](https://rubydoc.info/github/teamcapybara/capybara/master/Capybara%2FNode%2FMatchers:has_no_link%3F)
37
+ * [`has_button?`](https://rubydoc.info/github/teamcapybara/capybara/master/Capybara%2FNode%2FMatchers:has_button%3F)
38
+ * [`has_no_button?`](https://rubydoc.info/github/teamcapybara/capybara/master/Capybara%2FNode%2FMatchers:has_no_button%3F)
39
+ * [`has_field?`](https://rubydoc.info/github/teamcapybara/capybara/master/Capybara%2FNode%2FMatchers:has_field%3F)
40
+ * [`has_no_field?`](https://rubydoc.info/github/teamcapybara/capybara/master/Capybara%2FNode%2FMatchers:has_no_field%3F)
41
+ * [`has_checked_field?`](https://rubydoc.info/github/teamcapybara/capybara/master/Capybara%2FNode%2FMatchers:has_checked_field%3F)
42
+ * [`has_unchecked_field?`](https://rubydoc.info/github/teamcapybara/capybara/master/Capybara%2FNode%2FMatchers:has_unchecked_field%3F)
43
+ * [`has_no_table?`](https://rubydoc.info/github/teamcapybara/capybara/master/Capybara%2FNode%2FMatchers:has_no_table%3F)
44
+ * [`has_table?`](https://rubydoc.info/github/teamcapybara/capybara/master/Capybara%2FNode%2FMatchers:has_table%3F)
45
+ * [`has_select?`](https://rubydoc.info/github/teamcapybara/capybara/master/Capybara%2FNode%2FMatchers:has_select%3F)
46
+ * [`has_no_select?`](https://rubydoc.info/github/teamcapybara/capybara/master/Capybara%2FNode%2FMatchers:has_no_select%3F)
47
+ * [`has_selector?`](https://rubydoc.info/github/teamcapybara/capybara/master/Capybara%2FNode%2FMatchers:has_selector%3F)
48
+ * [`has_no_selector?`](https://rubydoc.info/github/teamcapybara/capybara/master/Capybara%2FNode%2FMatchers:has_no_selector%3F)
49
+ * [`has_no_checked_field?`](https://rubydoc.info/github/teamcapybara/capybara/master/Capybara%2FNode%2FMatchers:has_no_checked_field%3F)
50
+ * [`has_no_unchecked_field?`](https://rubydoc.info/github/teamcapybara/capybara/master/Capybara%2FNode%2FMatchers:has_no_unchecked_field%3F)
51
+
52
+ * Add support for `within*` Capybara DLS methods:
53
+
54
+ * [`within`](https://rubydoc.info/github/teamcapybara/capybara/master/Capybara%2FSession:within)
55
+ * [`within_element`](https://rubydoc.info/github/teamcapybara/capybara/master/Capybara%2FSession:within)
56
+ * [`within_fieldset`](https://rubydoc.info/github/teamcapybara/capybara/master/Capybara%2FSession:within_fieldset)
57
+ * [`within_table`](https://rubydoc.info/github/teamcapybara/capybara/master/Capybara%2FSession:within_table)
58
+
59
+ *Jacob Carlborg*
60
+
61
+ ## 2.58.0
62
+
63
+ * Switch to `standardrb`.
64
+
65
+ *Joel Hawksley*
66
+
67
+ * Add BootrAils article to resources.
68
+
69
+ *Joel Hawksley*
70
+
71
+ * Add @boardfish and @spone as maintainers.
72
+
73
+ *Joel Hawksley*, *Cameron Dutro*, *Blake Williams*
74
+
75
+ * Re-compile updated, inherited templates when class caching is disabled.
76
+
77
+ *Patrick Arnett*
78
+
79
+ * Add the latest version to the docs index.
80
+ * Improve the docs: add the versions various features were introduced in.
81
+
82
+ *Hans Lemuet*
83
+
84
+ * Update docs to reflect lack of block content support in controllers.
85
+
86
+ *Joel Hawksley*
87
+
88
+ * Prevent adding duplicates to `autoload_paths`.
89
+
90
+ *Thomas Hutterer*
91
+
92
+ * Add FreeAgent to list of companies using ViewComponent.
93
+
94
+ *Simon Fish*
95
+
96
+ * Include polymorphic slots in `ViewComponent::Base` by default.
97
+
98
+ *Cameron Dutro*
99
+
100
+ * Add per-component config option for stripping newlines from templates before compilation.
101
+
102
+ *Cameron Dutro*
103
+
104
+ * Add link to article by Matouš Borák.
105
+
106
+ *Joel Hawksley*
107
+
12
108
  ## 2.57.1
13
109
 
14
110
  * Fix issue causing `NoMethodError`s when calling helper methods from components rendered as part of a collection.
@@ -36,10 +36,10 @@ module ViewComponent
36
36
 
37
37
  def stimulus_controller
38
38
  if options["stimulus"]
39
- File.join(destination_directory, destination_file_name).
40
- sub("#{component_path}/", "").
41
- gsub("_", "-").
42
- gsub("/", "--")
39
+ File.join(destination_directory, destination_file_name)
40
+ .sub("#{component_path}/", "")
41
+ .tr("_", "-")
42
+ .gsub("/", "--")
43
43
  end
44
44
  end
45
45
 
@@ -16,6 +16,7 @@ module ViewComponent
16
16
  class Base < ActionView::Base
17
17
  include ActiveSupport::Configurable
18
18
  include ViewComponent::ContentAreas
19
+ include ViewComponent::PolymorphicSlots
19
20
  include ViewComponent::Previewable
20
21
  include ViewComponent::SlotableV2
21
22
  include ViewComponent::Translatable
@@ -31,6 +32,10 @@ module ViewComponent
31
32
  class_attribute :content_areas
32
33
  self.content_areas = [] # class_attribute:default doesn't work until Rails 5.2
33
34
 
35
+ # Config option that strips trailing whitespace in templates before compiling them.
36
+ class_attribute :__vc_strip_trailing_whitespace, instance_accessor: false, instance_predicate: false
37
+ self.__vc_strip_trailing_whitespace = false # class_attribute:default doesn't work until Rails 5.2
38
+
34
39
  attr_accessor :__vc_original_view_context
35
40
 
36
41
  # Components render in their own view context. Helpers and other functionality
@@ -135,8 +140,10 @@ module ViewComponent
135
140
  # Subclass components that call `super` inside their template code will cause a
136
141
  # double render if they emit the result:
137
142
  #
138
- # <%= super %> # double-renders
139
- # <% super %> # does not double-render
143
+ # ```erb
144
+ # <%= super %> # double-renders
145
+ # <% super %> # does not double-render
146
+ # ```
140
147
  #
141
148
  # Calls `super`, returning `nil` to avoid rendering the result twice.
142
149
  def render_parent
@@ -187,7 +194,8 @@ module ViewComponent
187
194
  end
188
195
 
189
196
  # @private
190
- def initialize(*); end
197
+ def initialize(*)
198
+ end
191
199
 
192
200
  # Re-use original view_context if we're not rendering a component.
193
201
  #
@@ -315,7 +323,9 @@ module ViewComponent
315
323
 
316
324
  # Set the controller used for testing components:
317
325
  #
318
- # config.view_component.test_controller = "MyTestController"
326
+ # ```ruby
327
+ # config.view_component.test_controller = "MyTestController"
328
+ # ```
319
329
  #
320
330
  # Defaults to ApplicationController. Can also be configured on a per-test
321
331
  # basis using `with_controller_class`.
@@ -325,13 +335,17 @@ module ViewComponent
325
335
 
326
336
  # Set if render monkey patches should be included or not in Rails <6.1:
327
337
  #
328
- # config.view_component.render_monkey_patch_enabled = false
338
+ # ```ruby
339
+ # config.view_component.render_monkey_patch_enabled = false
340
+ # ```
329
341
  #
330
342
  mattr_accessor :render_monkey_patch_enabled, instance_writer: false, default: true
331
343
 
332
344
  # Path for component files
333
345
  #
334
- # config.view_component.view_component_path = "app/my_components"
346
+ # ```ruby
347
+ # config.view_component.view_component_path = "app/my_components"
348
+ # ```
335
349
  #
336
350
  # Defaults to `app/components`.
337
351
  #
@@ -339,7 +353,9 @@ module ViewComponent
339
353
 
340
354
  # Parent class for generated components
341
355
  #
342
- # config.view_component.component_parent_class = "MyBaseComponent"
356
+ # ```ruby
357
+ # config.view_component.component_parent_class = "MyBaseComponent"
358
+ # ```
343
359
  #
344
360
  # Defaults to nil. If this is falsy, generators will use
345
361
  # "ApplicationComponent" if defined, "ViewComponent::Base" otherwise.
@@ -355,25 +371,33 @@ module ViewComponent
355
371
  #
356
372
  # Always generate a component with a sidecar directory:
357
373
  #
358
- # config.view_component.generate.sidecar = true
374
+ # ```ruby
375
+ # config.view_component.generate.sidecar = true
376
+ # ```
359
377
  #
360
378
  # #### #stimulus_controller
361
379
  #
362
380
  # Always generate a Stimulus controller alongside the component:
363
381
  #
364
- # config.view_component.generate.stimulus_controller = true
382
+ # ```ruby
383
+ # config.view_component.generate.stimulus_controller = true
384
+ # ```
365
385
  #
366
386
  # #### #locale
367
387
  #
368
388
  # Always generate translations file alongside the component:
369
389
  #
370
- # config.view_component.generate.locale = true
390
+ # ```ruby
391
+ # config.view_component.generate.locale = true
392
+ # ```
371
393
  #
372
394
  # #### #distinct_locale_files
373
395
  #
374
396
  # Always generate as many translations files as available locales:
375
397
  #
376
- # config.view_component.generate.distinct_locale_files = true
398
+ # ```ruby
399
+ # config.view_component.generate.distinct_locale_files = true
400
+ # ```
377
401
  #
378
402
  # One file will be generated for each configured `I18n.available_locales`,
379
403
  # falling back to `[:en]` when no `available_locales` is defined.
@@ -382,7 +406,9 @@ module ViewComponent
382
406
  #
383
407
  # Always generate preview alongside the component:
384
408
  #
385
- # config.view_component.generate.preview = true
409
+ # ```ruby
410
+ # config.view_component.generate.preview = true
411
+ # ```
386
412
  #
387
413
  # Defaults to `false`.
388
414
  mattr_accessor :generate, instance_writer: false, default: ActiveSupport::OrderedOptions.new(false)
@@ -436,7 +462,9 @@ module ViewComponent
436
462
 
437
463
  # Render a component for each element in a collection ([documentation](/guide/collections)):
438
464
  #
439
- # render(ProductsComponent.with_collection(@products, foo: :bar))
465
+ # ```ruby
466
+ # render(ProductsComponent.with_collection(@products, foo: :bar))
467
+ # ```
440
468
  #
441
469
  # @param collection [Enumerable] A list of items to pass the ViewComponent one at a time.
442
470
  # @param args [Arguments] Arguments to pass to the ViewComponent every time.
@@ -532,13 +560,35 @@ module ViewComponent
532
560
 
533
561
  # Set the parameter name used when rendering elements of a collection ([documentation](/guide/collections)):
534
562
  #
535
- # with_collection_parameter :item
563
+ # ```ruby
564
+ # with_collection_parameter :item
565
+ # ```
536
566
  #
537
567
  # @param parameter [Symbol] The parameter name used when rendering elements of a collection.
538
568
  def with_collection_parameter(parameter)
539
569
  @provided_collection_parameter = parameter
540
570
  end
541
571
 
572
+ # Strips trailing whitespace from templates before compiling them.
573
+ #
574
+ # ```ruby
575
+ # class MyComponent < ViewComponent::Base
576
+ # strip_trailing_whitespace
577
+ # end
578
+ # ```
579
+ #
580
+ # @param value [Boolean] Whether or not to strip newlines.
581
+ def strip_trailing_whitespace(value = true)
582
+ self.__vc_strip_trailing_whitespace = value
583
+ end
584
+
585
+ # Whether trailing whitespace will be stripped before compilation.
586
+ #
587
+ # @return [Boolean]
588
+ def strip_trailing_whitespace?
589
+ __vc_strip_trailing_whitespace
590
+ end
591
+
542
592
  # Ensure the component initializer accepts the
543
593
  # collection parameter. By default, we don't
544
594
  # validate that the default parameter name
@@ -586,11 +636,7 @@ module ViewComponent
586
636
 
587
637
  # @private
588
638
  def collection_parameter
589
- if provided_collection_parameter
590
- provided_collection_parameter
591
- else
592
- name && name.demodulize.underscore.chomp("_component").to_sym
593
- end
639
+ provided_collection_parameter || name && name.demodulize.underscore.chomp("_component").to_sym
594
640
  end
595
641
 
596
642
  # @private
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ViewComponent
4
+ # This is a simpler version of {Capybara::Session}.
5
+ #
6
+ # It only includes {Capybara::Node::Finders}, {Capybara::Node::Matchers},
7
+ # {#within} and {#within_element}. It is useful in that it does not require a
8
+ # session, an application or a driver, but can still use Capybara's finders
9
+ # and matchers on any string that contains HTML.
10
+ class CapybaraSimpleSession
11
+ # Most of the code in this class is shamelessly stolen from the
12
+ # {Capybara::Session} class in the Capybara gem
13
+ # (https://github.com/teamcapybara/capybara/blob/e704d00879fb1d1e1a0cc01e04c101bcd8af4a68/lib/capybara/session.rb#L38).
14
+
15
+ NODE_METHODS = %i[
16
+ all
17
+ first
18
+ text
19
+
20
+ find
21
+ find_all
22
+ find_button
23
+ find_by_id
24
+ find_field
25
+ find_link
26
+
27
+ has_content?
28
+ has_text?
29
+ has_css?
30
+ has_no_content?
31
+ has_no_text?
32
+ has_no_css?
33
+ has_no_xpath?
34
+ has_xpath?
35
+ has_link?
36
+ has_no_link?
37
+ has_button?
38
+ has_no_button?
39
+ has_field?
40
+ has_no_field?
41
+ has_checked_field?
42
+ has_unchecked_field?
43
+ has_no_table?
44
+ has_table?
45
+ has_select?
46
+ has_no_select?
47
+ has_selector?
48
+ has_no_selector?
49
+ has_no_checked_field?
50
+ has_no_unchecked_field?
51
+
52
+ assert_selector
53
+ assert_no_selector
54
+ assert_all_of_selectors
55
+ assert_none_of_selectors
56
+ assert_any_of_selectors
57
+ assert_text
58
+ assert_no_text
59
+ ].freeze
60
+
61
+ private_constant :NODE_METHODS
62
+
63
+ SESSION_METHODS = %i[within within_element within_fieldset within_table].freeze
64
+
65
+ private_constant :SESSION_METHODS
66
+
67
+ DSL_METHODS = (NODE_METHODS + SESSION_METHODS).freeze
68
+
69
+ # Stolen from: https://github.com/teamcapybara/capybara/blob/e704d00879fb1d1e1a0cc01e04c101bcd8af4a68/lib/capybara/session.rb#L767-L774.
70
+ NODE_METHODS.each do |method|
71
+ if RUBY_VERSION >= "2.7"
72
+ class_eval <<~METHOD, __FILE__, __LINE__ + 1
73
+ def #{method}(...)
74
+ current_scope.#{method}(...)
75
+ end
76
+ METHOD
77
+ else
78
+ define_method method do |*args, &block|
79
+ current_scope.send(method, *args, &block)
80
+ end
81
+ end
82
+ end
83
+
84
+ # Initializes the receiver with the given string of HTML.
85
+ #
86
+ # @param html [String] the HTML to create the session out of
87
+ def initialize(html)
88
+ @document = Capybara::Node::Simple.new(html)
89
+ end
90
+
91
+ # (see Capybara::Session#within)
92
+ def within(*args, **kw_args)
93
+ new_scope = args.first.respond_to?(:to_capybara_node) ? args.first.to_capybara_node : find(*args, **kw_args)
94
+ begin
95
+ scopes.push(new_scope)
96
+ yield if block_given?
97
+ ensure
98
+ scopes.pop
99
+ end
100
+ end
101
+
102
+ # (see Capybara::Session#within_element)
103
+ alias_method :within_element, :within
104
+
105
+ # (see Capybara::Session#within_fieldset)
106
+ def within_fieldset(locator, &block)
107
+ within(:fieldset, locator, &block)
108
+ end
109
+
110
+ # (see Capybara::Session#within_table)
111
+ def within_table(locator, &block)
112
+ within(:table, locator, &block)
113
+ end
114
+
115
+ # (see Capybara::Node::Element#native)
116
+ def native
117
+ current_scope.native
118
+ end
119
+
120
+ private
121
+
122
+ attr_reader :document
123
+
124
+ def scopes
125
+ @scopes ||= [nil]
126
+ end
127
+
128
+ def current_scope
129
+ scopes.last.presence || document
130
+ end
131
+ end
132
+ end
@@ -18,9 +18,9 @@ module ViewComponent
18
18
 
19
19
  def render_in(view_context, &block)
20
20
  components.map do |component|
21
- component.set_original_view_context(self.__vc_original_view_context)
21
+ component.set_original_view_context(__vc_original_view_context)
22
22
  component.render_in(view_context, &block)
23
- end.join.html_safe # rubocop:disable Rails/OutputSafety
23
+ end.join.html_safe
24
24
  end
25
25
 
26
26
  def components
@@ -61,7 +61,7 @@ module ViewComponent
61
61
  end
62
62
 
63
63
  def component_options(item, iterator)
64
- item_options = { component.collection_parameter => item }
64
+ item_options = {component.collection_parameter => item}
65
65
  item_options[component.collection_counter_parameter] = iterator.index + 1 if component.counter_argument_present?
66
66
  item_options[component.collection_iteration_parameter] = iterator.dup if component.iteration_argument_present?
67
67
 
@@ -31,6 +31,8 @@ module ViewComponent
31
31
  return if compiled? && !force
32
32
  return if component_class == ViewComponent::Base
33
33
 
34
+ component_class.superclass.compile(raise_errors: raise_errors) if should_compile_superclass?
35
+
34
36
  with_lock do
35
37
  subclass_instance_methods = component_class.instance_methods(false)
36
38
 
@@ -68,11 +70,13 @@ module ViewComponent
68
70
  component_class.send(:remove_method, method_name.to_sym)
69
71
  end
70
72
 
73
+ # rubocop:disable Style/EvalWithLocation
71
74
  component_class.class_eval <<-RUBY, template[:path], 0
72
75
  def #{method_name}
73
76
  #{compiled_template(template[:path])}
74
77
  end
75
78
  RUBY
79
+ # rubocop:enable Style/EvalWithLocation
76
80
  end
77
81
 
78
82
  define_render_template_for
@@ -151,15 +155,15 @@ module ViewComponent
151
155
  end
152
156
 
153
157
  invalid_variants =
154
- templates.
155
- group_by { |template| template[:variant] }.
156
- map { |variant, grouped| variant if grouped.length > 1 }.
157
- compact.
158
- sort
158
+ templates
159
+ .group_by { |template| template[:variant] }
160
+ .map { |variant, grouped| variant if grouped.length > 1 }
161
+ .compact
162
+ .sort
159
163
 
160
164
  unless invalid_variants.empty?
161
165
  errors <<
162
- "More than one template found for #{'variant'.pluralize(invalid_variants.count)} " \
166
+ "More than one template found for #{"variant".pluralize(invalid_variants.count)} " \
163
167
  "#{invalid_variants.map { |v| "'#{v}'" }.to_sentence} in #{component_class}. " \
164
168
  "There can only be one template file per variant."
165
169
  end
@@ -177,8 +181,8 @@ module ViewComponent
177
181
  count = duplicate_template_file_and_inline_variant_calls.count
178
182
 
179
183
  errors <<
180
- "Template #{'file'.pluralize(count)} and inline render #{'method'.pluralize(count)} " \
181
- "found for #{'variant'.pluralize(count)} " \
184
+ "Template #{"file".pluralize(count)} and inline render #{"method".pluralize(count)} " \
185
+ "found for #{"variant".pluralize(count)} " \
182
186
  "#{duplicate_template_file_and_inline_variant_calls.map { |v| "'#{v}'" }.to_sentence} " \
183
187
  "in #{component_class}. " \
184
188
  "There can only be a template file or inline render method per variant."
@@ -236,8 +240,9 @@ module ViewComponent
236
240
  end
237
241
 
238
242
  def compiled_template(file_path)
239
- handler = ActionView::Template.handler_for_extension(File.extname(file_path).gsub(".", ""))
243
+ handler = ActionView::Template.handler_for_extension(File.extname(file_path).delete("."))
240
244
  template = File.read(file_path)
245
+ template.rstrip! if component_class.strip_trailing_whitespace?
241
246
 
242
247
  if handler.method(:call).parameters.length > 1
243
248
  handler.call(component_class, template)
@@ -259,5 +264,14 @@ module ViewComponent
259
264
  "call"
260
265
  end
261
266
  end
267
+
268
+ def should_compile_superclass?
269
+ development? &&
270
+ templates.empty? &&
271
+ !(
272
+ component_class.instance_methods(false).include?(:call) ||
273
+ component_class.private_instance_methods(false).include?(:call)
274
+ )
275
+ end
262
276
  end
263
277
  end
@@ -21,7 +21,7 @@ module ViewComponent
21
21
  )
22
22
  end
23
23
 
24
- if block_given?
24
+ if block
25
25
  content = view_context.capture(&block)
26
26
  end
27
27
 
@@ -28,7 +28,7 @@ module ViewComponent
28
28
  end
29
29
 
30
30
  def types
31
- " → [#{@method.tag(:return).types.join(',')}]" if @method.tag(:return)&.types && show_types?
31
+ " → [#{@method.tag(:return).types.join(",")}]" if @method.tag(:return)&.types && show_types?
32
32
  end
33
33
 
34
34
  def signature_or_name
@@ -77,7 +77,8 @@ module ViewComponent
77
77
  options = app.config.view_component
78
78
 
79
79
  if options.show_previews && !options.preview_paths.empty?
80
- ActiveSupport::Dependencies.autoload_paths.concat(options.preview_paths)
80
+ paths_to_add = options.preview_paths - ActiveSupport::Dependencies.autoload_paths
81
+ ActiveSupport::Dependencies.autoload_paths.concat(paths_to_add) if paths_to_add.any?
81
82
  end
82
83
  end
83
84
 
@@ -133,10 +134,10 @@ module ViewComponent
133
134
 
134
135
  initializer "compiler mode" do |app|
135
136
  ViewComponent::Compiler.mode = if Rails.env.development? || Rails.env.test?
136
- ViewComponent::Compiler::DEVELOPMENT_MODE
137
- else
138
- ViewComponent::Compiler::PRODUCTION_MODE
139
- end
137
+ ViewComponent::Compiler::DEVELOPMENT_MODE
138
+ else
139
+ ViewComponent::Compiler::PRODUCTION_MODE
140
+ end
140
141
  end
141
142
 
142
143
  config.after_initialize do |app|
@@ -34,13 +34,14 @@ module ViewComponent
34
34
  def with_output_buffer(buf = nil)
35
35
  unless buf
36
36
  buf = ActionView::OutputBuffer.new
37
+ # rubocop:disable Style/SafeNavigation
37
38
  if output_buffer && output_buffer.respond_to?(:encoding)
38
39
  buf.force_encoding(output_buffer.encoding)
39
40
  end
41
+ # rubocop:enable Style/SafeNavigation
40
42
  end
41
43
 
42
44
  output_buffer.push(buf)
43
- result = nil
44
45
 
45
46
  begin
46
47
  yield
@@ -65,13 +66,13 @@ module ViewComponent
65
66
  def with_output_buffer(buf = nil)
66
67
  unless buf
67
68
  buf = ActionView::OutputBuffer.new
69
+ # rubocop:disable Style/SafeNavigation
68
70
  if @output_buffer && @output_buffer.respond_to?(:encoding)
69
71
  buf.force_encoding(@output_buffer.encoding)
70
72
  end
73
+ # rubocop:enable Style/SafeNavigation
71
74
  end
72
75
 
73
- result = nil
74
-
75
76
  if @output_buffer.is_a?(OutputBufferStack)
76
77
  @output_buffer.push(buf)
77
78
 
@@ -37,9 +37,7 @@ module ViewComponent
37
37
  end
38
38
 
39
39
  def safe_concat(arg)
40
- # rubocop:disable Rails/OutputSafety
41
40
  @current_buffer.safe_concat(arg)
42
- # rubocop:enable Rails/OutputSafety
43
41
  end
44
42
 
45
43
  def length
@@ -5,6 +5,17 @@ module ViewComponent
5
5
  # In older rails versions, using a concern isn't a good idea here because they appear to not work with
6
6
  # Module#prepend and class methods.
7
7
  def self.included(base)
8
+ if base != ViewComponent::Base
9
+ # :nocov:
10
+ location = Kernel.caller_locations(1, 1)[0]
11
+
12
+ warn(
13
+ "warning: ViewComponent::PolymorphicSlots is now included in ViewComponent::Base by default "\
14
+ "and can be removed from #{location.path}:#{location.lineno}"
15
+ )
16
+ # :nocov:
17
+ end
18
+
8
19
  base.singleton_class.prepend(ClassMethods)
9
20
  base.include(InstanceMethods)
10
21
  end
@@ -45,8 +56,12 @@ module ViewComponent
45
56
  "#{slot_name}_#{poly_type}"
46
57
  end
47
58
 
48
- # Deprecated: Will be removed in 3.0
49
59
  define_method(setter_name) do |*args, &block|
60
+ ViewComponent::Deprecation.warn(
61
+ "polymorphic slot setters like `#{setter_name}` are deprecated and will be removed in"\
62
+ "ViewComponent v3.0.0.\n\nUse `with_#{setter_name}` instead."
63
+ )
64
+
50
65
  set_polymorphic_slot(slot_name, poly_type, *args, &block)
51
66
  end
52
67
  ruby2_keywords(setter_name.to_sym) if respond_to?(:ruby2_keywords, true)
@@ -57,7 +72,7 @@ module ViewComponent
57
72
  ruby2_keywords(:"with_#{setter_name}") if respond_to?(:ruby2_keywords, true)
58
73
  end
59
74
 
60
- self.registered_slots[slot_name] = {
75
+ registered_slots[slot_name] = {
61
76
  collection: collection,
62
77
  renderable_hash: renderable_hash
63
78
  }
@@ -14,7 +14,7 @@ module ViewComponent # :nodoc:
14
14
  block: block,
15
15
  component: component,
16
16
  locals: {},
17
- template: "view_components/preview",
17
+ template: "view_components/preview"
18
18
  }
19
19
  end
20
20
 
@@ -66,10 +66,12 @@ module ViewComponent # :nodoc:
66
66
  name.chomp("Preview").underscore
67
67
  end
68
68
 
69
+ # rubocop:disable Style/TrivialAccessors
69
70
  # Setter for layout name.
70
71
  def layout(layout_name)
71
72
  @layout = layout_name
72
73
  end
74
+ # rubocop:enable Style/TrivialAccessors
73
75
 
74
76
  # Returns the relative path (from preview_path) to the preview example template if the template exists
75
77
  def preview_example_template_path(example)
@@ -87,15 +89,15 @@ module ViewComponent # :nodoc:
87
89
  end
88
90
 
89
91
  path = Dir["#{preview_path}/#{preview_name}_preview/#{example}.html.*"].first
90
- Pathname.new(path).
91
- relative_path_from(Pathname.new(preview_path)).
92
- to_s.
93
- sub(/\..*$/, "")
92
+ Pathname.new(path)
93
+ .relative_path_from(Pathname.new(preview_path))
94
+ .to_s
95
+ .sub(/\..*$/, "")
94
96
  end
95
97
 
96
98
  # Returns the method body for the example from the preview file.
97
99
  def preview_source(example)
98
- source = self.instance_method(example.to_sym).source.split("\n")
100
+ source = instance_method(example.to_sym).source.split("\n")
99
101
  source[1...(source.size - 1)].join("\n")
100
102
  end
101
103
 
@@ -3,7 +3,7 @@
3
3
  module ViewComponent
4
4
  module RenderComponentToStringHelper # :nodoc:
5
5
  def render_component_to_string(component)
6
- component.render_in(self.view_context)
6
+ component.render_in(view_context)
7
7
  end
8
8
  end
9
9
  end
@@ -4,7 +4,7 @@ module ViewComponent
4
4
  module RenderToStringMonkeyPatch # :nodoc:
5
5
  def render_to_string(options = {}, args = {})
6
6
  if options.respond_to?(:render_in)
7
- options.render_in(self.view_context)
7
+ options.render_in(view_context)
8
8
  else
9
9
  super
10
10
  end
@@ -3,7 +3,7 @@
3
3
  module ViewComponent
4
4
  module RenderingComponentHelper # :nodoc:
5
5
  def render_component(component)
6
- self.response_body = component.render_in(self.view_context)
6
+ self.response_body = component.render_in(view_context)
7
7
  end
8
8
  end
9
9
  end
@@ -4,7 +4,7 @@ module ViewComponent
4
4
  module RenderingMonkeyPatch # :nodoc:
5
5
  def render(options = {}, args = {})
6
6
  if options.respond_to?(:render_in)
7
- self.response_body = options.render_in(self.view_context)
7
+ self.response_body = options.render_in(view_context)
8
8
  else
9
9
  super
10
10
  end
@@ -30,7 +30,7 @@ module ViewComponent
30
30
 
31
31
  slot_names.each do |slot_name|
32
32
  # Ensure slot_name isn't already declared
33
- if self.slots.key?(slot_name)
33
+ if slots.key?(slot_name)
34
34
  raise ArgumentError.new("#{slot_name} slot declared multiple times")
35
35
  end
36
36
 
@@ -73,7 +73,7 @@ module ViewComponent
73
73
  class_name = "ViewComponent::Slot" unless class_name.present?
74
74
 
75
75
  # Register the slot on the component
76
- self.slots[slot_name] = {
76
+ slots[slot_name] = {
77
77
  class_name: class_name,
78
78
  instance_variable_name: instance_variable_name,
79
79
  collection: collection
@@ -84,7 +84,7 @@ module ViewComponent
84
84
  def inherited(child)
85
85
  # Clone slot configuration into child class
86
86
  # see #test_slots_pollution
87
- child.slots = self.slots.clone
87
+ child.slots = slots.clone
88
88
 
89
89
  super
90
90
  end
@@ -106,7 +106,7 @@ module ViewComponent
106
106
  #
107
107
  def slot(slot_name, **args, &block)
108
108
  # Raise ArgumentError if `slot` doesn't exist
109
- unless slots.keys.include?(slot_name)
109
+ unless slots.key?(slot_name)
110
110
  raise ArgumentError.new "Unknown slot '#{slot_name}' - expected one of '#{slots.keys}'"
111
111
  end
112
112
 
@@ -123,8 +123,7 @@ module ViewComponent
123
123
  slot_instance = args.present? ? slot_class.new(**args) : slot_class.new
124
124
 
125
125
  # Capture block and assign to slot_instance#content
126
- # rubocop:disable Rails/OutputSafety
127
- slot_instance.content = view_context.capture(&block).to_s.strip.html_safe if block_given?
126
+ slot_instance.content = view_context.capture(&block).to_s.strip.html_safe if block
128
127
 
129
128
  if slot[:collection]
130
129
  # Initialize instance variable as an empty array
@@ -9,7 +9,7 @@ module ViewComponent
9
9
 
10
10
  RESERVED_NAMES = {
11
11
  singular: %i[content render].freeze,
12
- plural: %i[contents renders].freeze,
12
+ plural: %i[contents renders].freeze
13
13
  }.freeze
14
14
 
15
15
  # Setup component slot state
@@ -190,20 +190,20 @@ module ViewComponent
190
190
  # Clone slot configuration into child class
191
191
  # see #test_slots_pollution
192
192
  def inherited(child)
193
- child.registered_slots = self.registered_slots.clone
193
+ child.registered_slots = registered_slots.clone
194
194
  super
195
195
  end
196
196
 
197
197
  private
198
198
 
199
199
  def register_slot(slot_name, **kwargs)
200
- self.registered_slots[slot_name] = define_slot(slot_name, **kwargs)
200
+ registered_slots[slot_name] = define_slot(slot_name, **kwargs)
201
201
  end
202
202
 
203
203
  def define_slot(slot_name, collection:, callable:)
204
204
  # Setup basic slot data
205
205
  slot = {
206
- collection: collection,
206
+ collection: collection
207
207
  }
208
208
  return slot unless callable
209
209
 
@@ -254,7 +254,7 @@ module ViewComponent
254
254
  end
255
255
 
256
256
  def raise_if_slot_registered(slot_name)
257
- if self.registered_slots.key?(slot_name)
257
+ if registered_slots.key?(slot_name)
258
258
  # TODO remove? This breaks overriding slots when slots are inherited
259
259
  raise ArgumentError.new(
260
260
  "#{self} declares the #{slot_name} slot multiple times.\n\n" \
@@ -287,8 +287,6 @@ module ViewComponent
287
287
 
288
288
  if slot[:collection]
289
289
  []
290
- else
291
- nil
292
290
  end
293
291
  end
294
292
 
@@ -305,7 +303,7 @@ module ViewComponent
305
303
  # 2. Since we've to pass block content to components when calling
306
304
  # `render`, evaluating the block here would require us to call
307
305
  # `view_context.capture` twice, which is slower
308
- slot.__vc_content_block = block if block_given?
306
+ slot.__vc_content_block = block if block
309
307
 
310
308
  # If class
311
309
  if slot_definition[:renderable]
@@ -321,7 +319,7 @@ module ViewComponent
321
319
  # methods like `content_tag` as well as parent component state.
322
320
  renderable_function = slot_definition[:renderable_function].bind(self)
323
321
  renderable_value =
324
- if block_given?
322
+ if block
325
323
  renderable_function.call(*args) do |*rargs|
326
324
  view_context.capture(*rargs, &block)
327
325
  end
@@ -1,15 +1,37 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "view_component/render_preview_helper"
4
+ require "view_component/capybara_simple_session"
4
5
 
5
6
  module ViewComponent
6
7
  module TestHelpers
7
8
  begin
8
9
  require "capybara/minitest"
10
+
9
11
  include Capybara::Minitest::Assertions
10
12
 
13
+ CapybaraSimpleSession::DSL_METHODS.each do |method|
14
+ if RUBY_VERSION >= "2.7"
15
+ class_eval <<~METHOD, __FILE__, __LINE__ + 1
16
+ def #{method}(...)
17
+ page.method("#{method}").call(...)
18
+ end
19
+ METHOD
20
+ else
21
+ define_method method do |*args, &block|
22
+ page.send method, *args, &block
23
+ end
24
+ end
25
+ end
26
+
27
+ def self.included(mod)
28
+ Capybara::Node::Simple.send(:define_method, :to_capybara_node) do
29
+ self
30
+ end
31
+ end
32
+
11
33
  def page
12
- Capybara::Node::Simple.new(@rendered_content)
34
+ @page ||= CapybaraSimpleSession.new(rendered_content)
13
35
  end
14
36
 
15
37
  def refute_component_rendered
@@ -32,6 +54,9 @@ module ViewComponent
32
54
  # @private
33
55
  attr_reader :rendered_content
34
56
 
57
+ # Returns the result of a render_inline call.
58
+ #
59
+ # @return [String]
35
60
  def rendered_component
36
61
  ViewComponent::Deprecation.warn(
37
62
  "`rendered_component` is deprecated and will be removed in v3.0.0. " \
@@ -52,6 +77,7 @@ module ViewComponent
52
77
  # @param component [ViewComponent::Base, ViewComponent::Collection] The instance of the component to be rendered.
53
78
  # @return [Nokogiri::HTML]
54
79
  def render_inline(component, **args, &block)
80
+ @page = nil
55
81
  @rendered_content =
56
82
  if Rails.version.to_f >= 6.1
57
83
  controller.view_context.render(component, args, &block)
@@ -73,6 +99,7 @@ module ViewComponent
73
99
  # assert_text("Hello, World!")
74
100
  # ```
75
101
  def render_in_view_context(&block)
102
+ @page = nil
76
103
  @rendered_content = controller.view_context.instance_exec(&block)
77
104
  Nokogiri::HTML.fragment(@rendered_content)
78
105
  end
@@ -9,7 +9,7 @@ module ViewComponent
9
9
  module Translatable
10
10
  extend ActiveSupport::Concern
11
11
 
12
- HTML_SAFE_TRANSLATION_KEY = /(?:_|\b)html\z/.freeze
12
+ HTML_SAFE_TRANSLATION_KEY = /(?:_|\b)html\z/
13
13
 
14
14
  included do
15
15
  class_attribute :i18n_backend, instance_writer: false, instance_predicate: false
@@ -23,14 +23,13 @@ module ViewComponent
23
23
  def build_i18n_backend
24
24
  return if CompileCache.compiled? self
25
25
 
26
- if (translation_files = _sidecar_files(%w[yml yaml])).any?
27
- self.i18n_backend = I18nBackend.new(
26
+ self.i18n_backend = if (translation_files = _sidecar_files(%w[yml yaml])).any?
27
+ # Returning nil cleans up if translations file has been removed since the last compilation
28
+
29
+ I18nBackend.new(
28
30
  i18n_scope: i18n_scope,
29
- load_paths: translation_files,
31
+ load_paths: translation_files
30
32
  )
31
- else
32
- # Cleanup if translations file has been removed since the last compilation
33
- self.i18n_backend = nil
34
33
  end
35
34
  end
36
35
  end
@@ -50,7 +49,7 @@ module ViewComponent
50
49
 
51
50
  def scope_data(data)
52
51
  @i18n_scope.reverse_each do |part|
53
- data = { part => data }
52
+ data = {part => data}
54
53
  end
55
54
  data
56
55
  end
@@ -95,7 +94,7 @@ module ViewComponent
95
94
  super(key, locale: locale, **options)
96
95
  end
97
96
  end
98
- alias :t :translate
97
+ alias_method :t, :translate
99
98
 
100
99
  # Exposes .i18n_scope as an instance method
101
100
  def i18n_scope
@@ -109,7 +108,7 @@ module ViewComponent
109
108
  # It's assumed here that objects loaded by the i18n backend will respond to `#html_safe?`.
110
109
  # It's reasonable that if we're in Rails, `active_support/core_ext/string/output_safety.rb`
111
110
  # will provide this to `Object`.
112
- translation.html_safe # rubocop:disable Rails/OutputSafety
111
+ translation.html_safe
113
112
  end
114
113
  end
115
114
 
@@ -3,8 +3,8 @@
3
3
  module ViewComponent
4
4
  module VERSION
5
5
  MAJOR = 2
6
- MINOR = 57
7
- PATCH = 1
6
+ MINOR = 59
7
+ PATCH = 0
8
8
 
9
9
  STRING = [MAJOR, MINOR, PATCH].join(".")
10
10
  end
@@ -7,6 +7,7 @@ module ViewComponent
7
7
  extend ActiveSupport::Autoload
8
8
 
9
9
  autoload :Base
10
+ autoload :CapybaraSimpleSession
10
11
  autoload :Compiler
11
12
  autoload :CompileCache
12
13
  autoload :ComponentError
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.57.1
4
+ version: 2.59.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: 2022-06-15 00:00:00.000000000 Z
11
+ date: 2022-07-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -185,19 +185,19 @@ dependencies:
185
185
  - !ruby/object:Gem::Version
186
186
  version: '13.0'
187
187
  - !ruby/object:Gem::Dependency
188
- name: rubocop-github
188
+ name: standard
189
189
  requirement: !ruby/object:Gem::Requirement
190
190
  requirements:
191
191
  - - "~>"
192
192
  - !ruby/object:Gem::Version
193
- version: 0.16.1
193
+ version: '1'
194
194
  type: :development
195
195
  prerelease: false
196
196
  version_requirements: !ruby/object:Gem::Requirement
197
197
  requirements:
198
198
  - - "~>"
199
199
  - !ruby/object:Gem::Version
200
- version: 0.16.1
200
+ version: '1'
201
201
  - !ruby/object:Gem::Dependency
202
202
  name: simplecov
203
203
  requirement: !ruby/object:Gem::Requirement
@@ -325,6 +325,7 @@ files:
325
325
  - lib/rails/generators/test_unit/templates/component_test.rb.tt
326
326
  - lib/view_component.rb
327
327
  - lib/view_component/base.rb
328
+ - lib/view_component/capybara_simple_session.rb
328
329
  - lib/view_component/collection.rb
329
330
  - lib/view_component/compile_cache.rb
330
331
  - lib/view_component/compiler.rb