view_component 4.0.0.alpha6 → 4.0.0.rc1

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dbca6043a0eebf7c6f63358c7b1c193ef59170b81a35eb9290f211d365bfb3cb
4
- data.tar.gz: 033db44d3dab35956b7ab8ac1a1a12673b7e82a0d4e0bbf3d3e791234dbcf757
3
+ metadata.gz: 7a112fdf735637f91b77151d96244fac83f0d78141df7a2a0ff8f2d72d0df2d0
4
+ data.tar.gz: 39c33408d7f256dc0da92edb17ca18460d776e6f1be89ba63d28e3ea102f8ba9
5
5
  SHA512:
6
- metadata.gz: ddd3ac59908bf697da69f8f0fa29b2e779e468a07a6f37e96240165797fce088bbf6937473363d349b7e48fa8011434377b52345c8f60204ff94a7a794a4fee8
7
- data.tar.gz: c8fac0e676aa9632453337a13169b690ed36bc9cd65e0de754c63f77878ba1b598c9a344f8f547a8b7c530c0e12c285de2fc0eee3afa4a1f7fe8805293414b8e
6
+ metadata.gz: b4c4f99335d6c2be623f1bd167e3d27567a7093acb388be1c245a6652423e4cc0ee8a16946da2d5ca9d35dd0283fecb1647888def129ec6fb75b82f427379558
7
+ data.tar.gz: 0ccb738fb45917ee49886ad2b1359a9bd3d98bf86d7e25f81422cffa5390514fe45d254a1e2d5900b6aa5649f428fa512d6a26c6d486d67546da8729dcc0d26f
@@ -8,7 +8,7 @@ module ViewComponent
8
8
  prepend_view_path File.expand_path("../../../views", __dir__)
9
9
 
10
10
  around_action :set_locale, only: :previews
11
- before_action :require_local!, unless: :show_previews?
11
+ before_action :require_local!, unless: :previews_enabled?
12
12
 
13
13
  content_security_policy(false)
14
14
 
@@ -51,8 +51,8 @@ module ViewComponent
51
51
  end
52
52
 
53
53
  # :doc:
54
- def show_previews?
55
- ViewComponent::Base.config.show_previews
54
+ def previews_enabled?
55
+ ViewComponent::Base.config.previews.enabled
56
56
  end
57
57
 
58
58
  # :doc:
@@ -0,0 +1 @@
1
+ <%= render(UrlForMailerComponent.new) %>
data/docs/CHANGELOG.md CHANGED
@@ -10,6 +10,111 @@ nav_order: 6
10
10
 
11
11
  ## main
12
12
 
13
+ ## 4.0.0.rc1
14
+
15
+ Almost six years after releasing [v1.0.0](https://github.com/ViewComponent/view_component/releases/tag/v1.0.0), we're proud to ship the first release candidate of ViewComponent 4. This release marks a shift towards a Long Term Support model for the project, having reached significant feature maturity. While contributions are always welcome, we're unlikely to accept further breaking changes or major feature additions.
16
+
17
+ Please report any issues at [https://github.com/ViewComponent/view_component/issues](https://github.com/ViewComponent/view_component/issues).
18
+
19
+ ### Breaking changes (production)
20
+
21
+ * Remove dependency on `ActionView::Base`, eliminating the need for capture compatibility patch. In some edge cases, this change may require switching to use the `helpers.` proxy.
22
+ * Require [non-EOL](https://endoflife.date/rails) Rails (`>= 7.1.0`) and Ruby (`>= 3.2.0`).
23
+ * Remove `render_component` and `render` monkey patch configured with `render_monkey_patch_enabled`.
24
+ * Remove deprecated `use_helper(s)`. Use `include MyHelper` or `helpers.` proxy instead.
25
+ * Support compatibility with `Dry::Initializer`. As a result, `EmptyOrInvalidInitializerError` will no longer be raised.
26
+ * Remove default initializer from `ViewComponent::Base`. Previously, `ViewComponent::Base` defined a catch-all initializer that allowed components without an initializer defined to be passed arbitrary arguments.
27
+ * Remove `use_deprecated_instrumentation_name` configuration option. Events will always use `render.view_component` name.
28
+ * Remove unnecessary `#format` methods that returned `nil`.
29
+ * Remove support for variant names containing `.` to be consistent with Rails.
30
+ * Rename internal methods to have `__vc_` prefix if they shouldn't be used by consumers. Make internal constants private. Make `Collection#components`, `Slotable#register_polymorphic_slot` private. Remove unused `ComponentError` class.
31
+ * Use ActionView's `lookup_context` for picking templates instead of the request format.
32
+
33
+ 3.15 added support for using templates that match the request format, that is if `/resource.csv` is requested then
34
+ ViewComponents would pick `_component.csv.erb` over `_component.html.erb`.
35
+
36
+ With this release, the request format is no longer considered and instead ViewComponent will use the Rails logic for picking the most appropriate template type, that is the csv template will be used if it matches the `Accept` header or because the controller uses a `respond_to` block to pick the response format.
37
+
38
+ ### Breaking changes (dev/test)
39
+
40
+ * Rename `config.generate.component_parent_class` to `config.generate.parent_class`.
41
+ * Remove `config.test_controller` in favor of `vc_test_controller_class` test helper method.
42
+ * `config.component_parent_class` is now `config.generate.component_parent_class`, moving the generator-specific option to the generator configuration namespace.
43
+ * Move previews-related configuration (`enabled`, `route`, `paths`, `default_layout`, `controller`) to under `previews` namespace.
44
+ * `config.view_component_path` is now `config.generate.path`, as components have long since been able to exist in any directory.
45
+ * `--inline` generator option now generates inline template. Use `--call` to generate `#call` method.
46
+ * Remove broken integration with `rails stats` that ignored components outside of `app/components`.
47
+ * Remove `preview_source` functionality. Consider using [Lookbook](https://lookbook.build/) instead.
48
+ * Use `Nokogiri::HTML5` instead of `Nokogiri::HTML4` for test helpers.
49
+ * Move generators to a ViewComponent namespace.
50
+
51
+ Before, ViewComponent generators pollute the generator namespace with a bunch of top level items, and claim the generic "component" name.
52
+
53
+ Now, generators live in a "view_component" module/namespace, so what was before `rails g
54
+ component` is now `rails g view_component:component`.
55
+
56
+ ### New features
57
+
58
+ * Add `SystemSpecHelpers` for use with RSpec.
59
+ * Add support for including `Turbo::StreamsHelper`.
60
+ * Add template annotations for components with `def call`.
61
+ * Graduate `SlotableDefault` to be included by default.
62
+ * Add `#current_template` accessor and `Template#path` for diagnostic usage.
63
+ * Reduce string allocations during compilation.
64
+
65
+ ### Bug fixes
66
+
67
+ * Fix bug where virtual path wasn't reset, breaking translations outside of components.
68
+ * Fix bug where `config.previews.enabled` didn't function properly in production environments.
69
+ * Fix bug where response format wasn't set, which caused issues with Turbo Frames.
70
+ * Fix bug in `SlotableDefault` where default couldn't be overridden when content was passed as a block.
71
+ * Fix bug where request-aware helpers didn't work outside of the request context.
72
+ * `ViewComponentsSystemTestController` shouldn't be useable outside of test environment
73
+
74
+ ### Non-functional changes
75
+
76
+ * Remove unnecessary usage of `ruby2_keywords`.
77
+ * Remove unnecessary `respond_to` checks.
78
+ * Require MFA when publishing to RubyGems.
79
+ * Clean up project dependencies, relaxing versions of development gems.
80
+ * Add test case for absolute URL path helpers in mailers.
81
+ * Update documentation on performance to reflect more representative benchmark showing 2-3x speed increase over partials.
82
+ * Add documentation note about instrumentation negatively affecting performance.
83
+ * Remove unnecessary ENABLE_RELOADING test suite flag.
84
+ * `config.previews.default_layout` should default to nil.
85
+ * Add test coverage for uncovered code.
86
+ * Test against `turbo-rails` `v2` and `rspec-rails` `v7`.
87
+
88
+ ## 4.0.0.alpha7
89
+
90
+ * BREAKING: Remove deprecated `use_helper(s)`. Use `include MyHelper` or `helpers.` proxy instead.
91
+
92
+ *Joel Hawksley*
93
+
94
+ * BREAKING: Support compatibility with `Dry::Initializer`. As a result, `EmptyOrInvalidInitializerError` will no longer be raised.
95
+
96
+ *Joel Hawksley*
97
+
98
+ * BREAKING: Rename `config.generate.component_parent_class` to `config.generate.parent_class`.
99
+
100
+ *Joel Hawksley*
101
+
102
+ * Fix bug where `config.previews.enabled` didn't function properly in production environments.
103
+
104
+ *Joel Hawksley*
105
+
106
+ * `config.previews.default_layout` should default to nil.
107
+
108
+ *Joel Hawksley*
109
+
110
+ * Add test case for absolute URL path helpers in mailers.
111
+
112
+ *Joel Hawksley*
113
+
114
+ * Fix bug where response format wasn't set, which caused issues with Turbo Frames.
115
+
116
+ *Joel Hawksley*
117
+
13
118
  ## 4.0.0.alpha6
14
119
 
15
120
  * BREAKING: Remove `config.test_controller` in favor of `vc_test_controller_class` test helper method.
@@ -129,11 +234,11 @@ This release makes the following breaking changes:
129
234
 
130
235
  * BREAKING: Use ActionView's `lookup_context` for picking templates instead of the request format.
131
236
 
132
- 3.15 added support for using templates that match the request format, i.e. if `/resource.csv` is requested then
237
+ 3.15 added support for using templates that match the request format, that is if `/resource.csv` is requested then
133
238
  ViewComponents would pick `_component.csv.erb` over `_component.html.erb`.
134
239
 
135
240
  With this release, the request format is no longer considered and instead ViewComponent will use the Rails logic
136
- for picking the most appropriate template type, i.e. the csv template will be used if it matches the `Accept` header
241
+ for picking the most appropriate template type, that is the csv template will be used if it matches the `Accept` header
137
242
  or because the controller uses a `respond_to` block to pick the response format.
138
243
 
139
244
  *Stephen Nelson*
@@ -142,11 +247,11 @@ This release makes the following breaking changes:
142
247
 
143
248
  *Joel Hawksley*
144
249
 
145
- * Fix bug where request-aware helpers did not work outside of the request context.
250
+ * Fix bug where request-aware helpers didn't work outside of the request context.
146
251
 
147
252
  *Joel Hawksley*, *Stephen Nelson*
148
253
 
149
- * `ViewComponentsSystemTestController` should not be useable outside of test environment
254
+ * `ViewComponentsSystemTestController` shouldn't be useable outside of test environment
150
255
 
151
256
  *Joel Hawksley*, *Stephen Nelson*
152
257
 
@@ -186,6 +291,10 @@ This release makes the following breaking changes:
186
291
 
187
292
  *Simon Fish*
188
293
 
294
+ * Deprecate `use_helper(s)`. Use `include MyHelper` or `helpers.` proxy instead.
295
+
296
+ *Joel Hawksley*
297
+
189
298
  * Reduce string allocations during compilation.
190
299
 
191
300
  *Jonathan del Strother*
@@ -14,7 +14,6 @@ require "view_component/slotable"
14
14
  require "view_component/template"
15
15
  require "view_component/translatable"
16
16
  require "view_component/with_content_helper"
17
- require "view_component/use_helpers"
18
17
 
19
18
  module ActionView
20
19
  class OutputBuffer
@@ -48,11 +47,11 @@ module ViewComponent
48
47
  end
49
48
 
50
49
  include ActionView::Helpers
50
+ include Rails.application.routes.url_helpers if defined?(Rails) && Rails.application
51
51
  include ERB::Escape
52
52
  include ActiveSupport::CoreExt::ERBUtil
53
53
 
54
54
  include ViewComponent::InlineTemplate
55
- include ViewComponent::UseHelpers
56
55
  include ViewComponent::Slotable
57
56
  include ViewComponent::Translatable
58
57
  include ViewComponent::WithContentHelper
@@ -70,8 +69,9 @@ module ViewComponent
70
69
  delegate :content_security_policy_nonce, to: :helpers
71
70
 
72
71
  # Config option that strips trailing whitespace in templates before compiling them.
73
- class_attribute :__vc_strip_trailing_whitespace, instance_accessor: false, instance_predicate: false
74
- self.__vc_strip_trailing_whitespace = false # class_attribute:default doesn't work until Rails 5.2
72
+ class_attribute :__vc_strip_trailing_whitespace, instance_accessor: false, instance_predicate: false, default: false
73
+
74
+ class_attribute :__vc_response_format, instance_accessor: false, instance_predicate: false, default: nil
75
75
 
76
76
  attr_accessor :__vc_original_view_context
77
77
  attr_reader :current_template
@@ -91,6 +91,11 @@ module ViewComponent
91
91
 
92
92
  using RequestDetails
93
93
 
94
+ # Including `Rails.application.routes.url_helpers` defines an initializer that accepts (...),
95
+ # so we have to define our own empty initializer to overwrite it.
96
+ def initialize
97
+ end
98
+
94
99
  # Entrypoint for rendering components.
95
100
  #
96
101
  # - `view_context`: ActionView context from calling view
@@ -103,6 +108,7 @@ module ViewComponent
103
108
  self.class.__vc_compile(raise_errors: true)
104
109
 
105
110
  @view_context = view_context
111
+ old_virtual_path = view_context.instance_variable_get(:@virtual_path)
106
112
  self.__vc_original_view_context ||= view_context
107
113
 
108
114
  @output_buffer = view_context.output_buffer
@@ -135,13 +141,15 @@ module ViewComponent
135
141
  value = nil
136
142
 
137
143
  @output_buffer.with_buffer do
144
+ @view_context.instance_variable_set(:@virtual_path, virtual_path)
145
+
138
146
  rendered_template = render_template_for(@__vc_requested_details).to_s
139
147
 
140
148
  # Avoid allocating new string when output_preamble and output_postamble are blank
141
149
  value = if output_preamble.blank? && output_postamble.blank?
142
150
  rendered_template
143
151
  else
144
- safe_output_preamble + rendered_template + safe_output_postamble
152
+ __vc_safe_output_preamble + rendered_template + __vc_safe_output_postamble
145
153
  end
146
154
  end
147
155
 
@@ -155,6 +163,7 @@ module ViewComponent
155
163
  ""
156
164
  end
157
165
  ensure
166
+ view_context.instance_variable_set(:@virtual_path, old_virtual_path)
158
167
  @current_template = old_current_template
159
168
  end
160
169
 
@@ -334,6 +343,10 @@ module ViewComponent
334
343
  __vc_render_in_block_provided? || __vc_content_set_by_with_content_defined?
335
344
  end
336
345
 
346
+ def format
347
+ self.class.__vc_response_format
348
+ end
349
+
337
350
  private
338
351
 
339
352
  attr_reader :view_context
@@ -346,11 +359,7 @@ module ViewComponent
346
359
  defined?(@__vc_content_set_by_with_content)
347
360
  end
348
361
 
349
- def content_evaluated?
350
- defined?(@__vc_content_evaluated) && @__vc_content_evaluated
351
- end
352
-
353
- def maybe_escape_html(text)
362
+ def __vc_maybe_escape_html(text)
354
363
  return text if @current_template && !@current_template.html?
355
364
  return text if text.blank?
356
365
 
@@ -362,14 +371,14 @@ module ViewComponent
362
371
  end
363
372
  end
364
373
 
365
- def safe_output_preamble
366
- maybe_escape_html(output_preamble) do
374
+ def __vc_safe_output_preamble
375
+ __vc_maybe_escape_html(output_preamble) do
367
376
  Kernel.warn("WARNING: The #{self.class} component was provided an HTML-unsafe preamble. The preamble will be automatically escaped, but you may want to investigate.")
368
377
  end
369
378
  end
370
379
 
371
- def safe_output_postamble
372
- maybe_escape_html(output_postamble) do
380
+ def __vc_safe_output_postamble
381
+ __vc_maybe_escape_html(output_postamble) do
373
382
  Kernel.warn("WARNING: The #{self.class} component was provided an HTML-unsafe postamble. The postamble will be automatically escaped, but you may want to investigate.")
374
383
  end
375
384
  end
@@ -432,12 +441,12 @@ module ViewComponent
432
441
  #
433
442
  # Defaults to `false`.
434
443
  #
435
- # #### component_parent_class
444
+ # #### ßparent_class
436
445
  #
437
446
  # Parent class for generated components
438
447
  #
439
448
  # ```ruby
440
- # config.view_component.generate.component_parent_class = "MyBaseComponent"
449
+ # config.view_component.generate.parent_class = "MyBaseComponent"
441
450
  # ```
442
451
  #
443
452
  # Defaults to nil. If this is falsy, generators will use
@@ -511,6 +520,11 @@ module ViewComponent
511
520
  Collection.new(self, collection, spacer_component, **args)
512
521
  end
513
522
 
523
+ # @private
524
+ def __vc_compile(raise_errors: false, force: false)
525
+ __vc_compiler.compile(raise_errors: raise_errors, force: force)
526
+ end
527
+
514
528
  # @private
515
529
  def inherited(child)
516
530
  # Compile so child will inherit compiled `call_*` template methods that
@@ -533,12 +547,6 @@ module ViewComponent
533
547
  RUBY
534
548
  end
535
549
 
536
- # If Rails application is loaded, add application url_helpers to the component context
537
- # we need to check this to use this gem as a dependency
538
- if defined?(Rails) && Rails.application && !(child < Rails.application.routes.url_helpers)
539
- child.include Rails.application.routes.url_helpers
540
- end
541
-
542
550
  # Derive the source location of the component Ruby file from the call stack.
543
551
  # We need to ignore `inherited` frames here as they indicate that `inherited`
544
552
  # has been re-defined by the consuming application, likely in ApplicationComponent.
@@ -548,7 +556,7 @@ module ViewComponent
548
556
  child.virtual_path = child.name&.underscore
549
557
 
550
558
  # Set collection parameter to the extended component
551
- child.with_collection_parameter provided_collection_parameter
559
+ child.with_collection_parameter(__vc_provided_collection_parameter)
552
560
 
553
561
  if instance_methods(false).include?(:render_template_for)
554
562
  vc_ancestor_calls = defined?(@__vc_ancestor_calls) ? @__vc_ancestor_calls.dup : []
@@ -570,11 +578,6 @@ module ViewComponent
570
578
  __vc_compile unless __vc_compiled?
571
579
  end
572
580
 
573
- # @private
574
- def __vc_compile(raise_errors: false, force: false)
575
- __vc_compiler.compile(raise_errors: raise_errors, force: force)
576
- end
577
-
578
581
  # @private
579
582
  def __vc_compiler
580
583
  @__vc_compiler ||= Compiler.new(self)
@@ -588,8 +591,8 @@ module ViewComponent
588
591
  #
589
592
  # @param parameter [Symbol] The parameter name used when rendering elements of a collection.
590
593
  def with_collection_parameter(parameter)
591
- @provided_collection_parameter = parameter
592
- @initialize_parameters = nil
594
+ @__vc_provided_collection_parameter = parameter
595
+ @__vc_initialize_parameters = nil
593
596
  end
594
597
 
595
598
  # Strips trailing whitespace from templates before compiling them.
@@ -619,17 +622,10 @@ module ViewComponent
619
622
  # rendering is optional.
620
623
  # @private
621
624
  def __vc_validate_collection_parameter!(validate_default: false)
622
- parameter = validate_default ? __vc_collection_parameter : provided_collection_parameter
625
+ parameter = validate_default ? __vc_collection_parameter : __vc_provided_collection_parameter
623
626
 
624
627
  return unless parameter
625
- return if initialize_parameter_names.include?(parameter) || splatted_keyword_argument_present?
626
-
627
- # If Ruby can't parse the component class, then the initialize
628
- # parameters will be empty and ViewComponent will not be able to render
629
- # the component.
630
- if initialize_parameters.empty?
631
- raise EmptyOrInvalidInitializerError.new(name, parameter)
632
- end
628
+ return if __vc_initialize_parameter_names.include?(parameter) || __vc_splatted_keyword_argument_present?
633
629
 
634
630
  raise MissingCollectionArgumentError.new(name, parameter)
635
631
  end
@@ -639,55 +635,57 @@ module ViewComponent
639
635
  # methods.
640
636
  # @private
641
637
  def __vc_validate_initialization_parameters!
642
- return unless initialize_parameter_names.include?(:content)
638
+ return unless __vc_initialize_parameter_names.include?(:content)
643
639
 
644
640
  raise ReservedParameterError.new(name, :content)
645
641
  end
646
642
 
647
643
  # @private
648
644
  def __vc_collection_parameter
649
- provided_collection_parameter || name && name.demodulize.underscore.chomp("_component").to_sym
645
+ @__vc_provided_collection_parameter ||= name && name.demodulize.underscore.chomp("_component").to_sym
650
646
  end
651
647
 
652
648
  # @private
653
649
  def __vc_collection_counter_parameter
654
- :"#{__vc_collection_parameter}_counter"
650
+ @__vc_collection_counter_parameter ||= :"#{__vc_collection_parameter}_counter"
655
651
  end
656
652
 
657
653
  # @private
658
654
  def __vc_counter_argument_present?
659
- initialize_parameter_names.include?(__vc_collection_counter_parameter)
655
+ __vc_initialize_parameter_names.include?(__vc_collection_counter_parameter)
660
656
  end
661
657
 
662
658
  # @private
663
659
  def __vc_collection_iteration_parameter
664
- :"#{__vc_collection_parameter}_iteration"
660
+ @__vc_collection_iteration_parameter ||= :"#{__vc_collection_parameter}_iteration"
665
661
  end
666
662
 
667
663
  # @private
668
664
  def __vc_iteration_argument_present?
669
- initialize_parameter_names.include?(__vc_collection_iteration_parameter)
665
+ __vc_initialize_parameter_names.include?(__vc_collection_iteration_parameter)
670
666
  end
671
667
 
672
668
  private
673
669
 
674
- def splatted_keyword_argument_present?
675
- initialize_parameters.flatten.include?(:keyrest) &&
676
- !initialize_parameters.include?([:keyrest, :**]) # Un-named splatted keyword args don't count!
670
+ def __vc_splatted_keyword_argument_present?
671
+ __vc_initialize_parameters.flatten.include?(:keyrest)
677
672
  end
678
673
 
679
- def initialize_parameter_names
680
- return attribute_names.map(&:to_sym) if respond_to?(:attribute_names)
681
-
682
- initialize_parameters.map(&:last)
674
+ def __vc_initialize_parameter_names
675
+ @__vc_initialize_parameter_names ||=
676
+ if respond_to?(:attribute_names)
677
+ attribute_names.map(&:to_sym)
678
+ else
679
+ __vc_initialize_parameters.map(&:last)
680
+ end
683
681
  end
684
682
 
685
- def initialize_parameters
686
- @initialize_parameters ||= instance_method(:initialize).parameters
683
+ def __vc_initialize_parameters
684
+ @__vc_initialize_parameters ||= instance_method(:initialize).parameters
687
685
  end
688
686
 
689
- def provided_collection_parameter
690
- @provided_collection_parameter ||= nil
687
+ def __vc_provided_collection_parameter
688
+ @__vc_provided_collection_parameter ||= nil
691
689
  end
692
690
  end
693
691
 
@@ -48,7 +48,16 @@ module ViewComponent
48
48
 
49
49
  define_render_template_for
50
50
 
51
- @component.register_default_slots
51
+ # Set the format if the component only responds to a single format.
52
+ # Unfortunately we cannot determine which format a multi-format
53
+ # component will respond to until render time, so those components
54
+ # will not set the response format.
55
+ #
56
+ # TODO: Investigate upstream changes necessary to support multi-format renderables
57
+ unique_formats = templates.map(&:format).uniq
58
+ @component.__vc_response_format = unique_formats.last if unique_formats.one?
59
+
60
+ @component.__vc_register_default_slots
52
61
  @component.__vc_build_i18n_backend
53
62
 
54
63
  CompileCache.register(@component)
@@ -109,6 +118,7 @@ module ViewComponent
109
118
  errors << "Couldn't find a template file or inline render method for #{@component}." if @templates.empty?
110
119
 
111
120
  @templates
121
+ .reject { |template| template.inline_call? && !template.defined_on_self? }
112
122
  .map { |template| [template.variant, template.format] }
113
123
  .tally
114
124
  .select { |_, count| count > 1 }
@@ -167,10 +177,10 @@ module ViewComponent
167
177
 
168
178
  def gather_templates
169
179
  @templates ||=
170
- if @component.inline_template.present?
180
+ if @component.__vc_inline_template.present?
171
181
  [Template::Inline.new(
172
182
  component: @component,
173
- inline_template: @component.inline_template
183
+ inline_template: @component.__vc_inline_template
174
184
  )]
175
185
  else
176
186
  path_parser = ActionView::Resolver::PathParser.new
@@ -118,9 +118,9 @@ module ViewComponent
118
118
  #
119
119
  # #### `#default_layout`
120
120
  #
121
- # A custom default layout used for the previews index page and individual previews. Defaults to `false`:
121
+ # A custom default layout used for the previews index page and individual previews. Defaults to `nil`:
122
122
  #
123
- # config.view_component.previews.default_layout = false
123
+ # config.view_component.previews.default_layout = "preview_layout"
124
124
  #
125
125
 
126
126
  # @!attribute instrumentation_enabled
@@ -128,12 +128,6 @@ module ViewComponent
128
128
  # Whether ActiveSupport notifications are enabled.
129
129
  # Defaults to `false`.
130
130
 
131
- # @!attribute test_controller
132
- # @return [String]
133
- # The controller used for testing components.
134
- # Can also be configured on a per-test basis using `#with_controller_class`.
135
- # Defaults to `ApplicationController`.
136
-
137
131
  def default_preview_paths
138
132
  (default_rails_preview_paths + default_rails_engines_preview_paths).uniq
139
133
  end
@@ -170,7 +164,7 @@ module ViewComponent
170
164
  options.controller = "ViewComponentsController"
171
165
  options.route = "/rails/view_components"
172
166
  options.enabled = Rails.env.development? || Rails.env.test?
173
- options.default_layout = false
167
+ options.default_layout = nil
174
168
  options.paths = default_preview_paths
175
169
  options
176
170
  end
@@ -5,7 +5,7 @@ module ViewComponent
5
5
  extend ActiveSupport::Concern
6
6
 
7
7
  included do
8
- next if respond_to?(:config) && config.respond_to?(:view_component) && config.respond_to_missing?(:test_controller)
8
+ next if respond_to?(:config) && config.respond_to?(:view_component) && config.respond_to_missing?(:instrumentation_enabled)
9
9
 
10
10
  include ActiveSupport::Configurable
11
11
 
@@ -15,9 +15,9 @@ module ViewComponent
15
15
  options[config_option] ||= ViewComponent::Base.public_send(config_option)
16
16
  end
17
17
  options.instrumentation_enabled = false if options.instrumentation_enabled.nil?
18
- options.show_previews = (Rails.env.development? || Rails.env.test?) if options.show_previews.nil?
18
+ options.previews.enabled = (Rails.env.development? || Rails.env.test?) if options.previews.enabled.nil?
19
19
 
20
- if options.show_previews
20
+ if options.previews.enabled
21
21
  # This is still necessary because when `config.view_component` is declared, `Rails.root` is unspecified.
22
22
  options.previews.paths << "#{Rails.root}/test/components/previews" if defined?(Rails.root) && Dir.exist?(
23
23
  "#{Rails.root}/test/components/previews"
@@ -36,7 +36,7 @@ module ViewComponent
36
36
  initializer "view_component.set_autoload_paths" do |app|
37
37
  options = app.config.view_component
38
38
 
39
- if options.show_previews && !options.previews.paths.empty?
39
+ if options.previews.enabled && !options.previews.paths.empty?
40
40
  paths_to_add = options.previews.paths - ActiveSupport::Dependencies.autoload_paths
41
41
  ActiveSupport::Dependencies.autoload_paths.concat(paths_to_add) if paths_to_add.any?
42
42
  end
@@ -88,7 +88,7 @@ module ViewComponent
88
88
  config.after_initialize do |app|
89
89
  options = app.config.view_component
90
90
 
91
- if options.show_previews
91
+ if options.previews.enabled
92
92
  app.routes.prepend do
93
93
  preview_controller = options.previews.controller.sub(/Controller$/, "").underscore
94
94
 
@@ -65,18 +65,6 @@ module ViewComponent
65
65
  end
66
66
  end
67
67
 
68
- class EmptyOrInvalidInitializerError < StandardError
69
- MESSAGE =
70
- "The COMPONENT initializer is empty or invalid. " \
71
- "It must accept the parameter `PARAMETER` to render it as a collection.\n\n" \
72
- "To fix this issue, update the initializer to accept `PARAMETER`.\n\n" \
73
- "See [the collections docs](https://viewcomponent.org/guide/collections.html) for more information on rendering collections."
74
-
75
- def initialize(klass_name, parameter)
76
- super(MESSAGE.gsub("COMPONENT", klass_name.to_s).gsub("PARAMETER", parameter.to_s))
77
- end
78
- end
79
-
80
68
  class MissingCollectionArgumentError < StandardError
81
69
  MESSAGE =
82
70
  "The initializer for COMPONENT doesn't accept the parameter `PARAMETER`, " \
@@ -37,7 +37,7 @@ module ViewComponent # :nodoc:
37
37
  method.end_with?("_template") || super
38
38
  end
39
39
 
40
- def inline_template
40
+ def __vc_inline_template
41
41
  @__vc_inline_template if defined?(@__vc_inline_template)
42
42
  end
43
43
 
@@ -73,25 +73,25 @@ module ViewComponent
73
73
  #
74
74
  # <%= render_inline(MyComponent.new.with_header_content("Foo")) %>
75
75
  def renders_one(slot_name, callable = nil)
76
- validate_singular_slot_name(slot_name)
76
+ __vc_validate_singular_slot_name(slot_name)
77
77
 
78
78
  if callable.is_a?(Hash) && callable.key?(:types)
79
- register_polymorphic_slot(slot_name, callable[:types], collection: false)
79
+ __vc_register_polymorphic_slot(slot_name, callable[:types], collection: false)
80
80
  else
81
- validate_plural_slot_name(ActiveSupport::Inflector.pluralize(slot_name).to_sym)
81
+ __vc_validate_plural_slot_name(ActiveSupport::Inflector.pluralize(slot_name).to_sym)
82
82
 
83
83
  setter_method_name = :"with_#{slot_name}"
84
84
 
85
85
  define_method setter_method_name do |*args, **kwargs, &block|
86
- set_slot(slot_name, nil, *args, **kwargs, &block)
86
+ __vc_set_slot(slot_name, nil, *args, **kwargs, &block)
87
87
  end
88
88
 
89
89
  self::GeneratedSlotMethods.define_method slot_name do
90
- get_slot(slot_name)
90
+ __vc_get_slot(slot_name)
91
91
  end
92
92
 
93
93
  self::GeneratedSlotMethods.define_method :"#{slot_name}?" do
94
- get_slot(slot_name).present?
94
+ __vc_get_slot(slot_name).present?
95
95
  end
96
96
 
97
97
  define_method :"with_#{slot_name}_content" do |content|
@@ -100,7 +100,7 @@ module ViewComponent
100
100
  self
101
101
  end
102
102
 
103
- register_slot(slot_name, collection: false, callable: callable)
103
+ __vc_register_slot(slot_name, collection: false, callable: callable)
104
104
  end
105
105
  end
106
106
 
@@ -142,18 +142,18 @@ module ViewComponent
142
142
  # <% end %>
143
143
  # <% end %>
144
144
  def renders_many(slot_name, callable = nil)
145
- validate_plural_slot_name(slot_name)
145
+ __vc_validate_plural_slot_name(slot_name)
146
146
 
147
147
  if callable.is_a?(Hash) && callable.key?(:types)
148
- register_polymorphic_slot(slot_name, callable[:types], collection: true)
148
+ __vc_register_polymorphic_slot(slot_name, callable[:types], collection: true)
149
149
  else
150
150
  singular_name = ActiveSupport::Inflector.singularize(slot_name)
151
- validate_singular_slot_name(ActiveSupport::Inflector.singularize(slot_name).to_sym)
151
+ __vc_validate_singular_slot_name(ActiveSupport::Inflector.singularize(slot_name).to_sym)
152
152
 
153
153
  setter_method_name = :"with_#{singular_name}"
154
154
 
155
155
  define_method setter_method_name do |*args, **kwargs, &block|
156
- set_slot(slot_name, nil, *args, **kwargs, &block)
156
+ __vc_set_slot(slot_name, nil, *args, **kwargs, &block)
157
157
  end
158
158
 
159
159
  define_method :"with_#{singular_name}_content" do |content|
@@ -165,22 +165,22 @@ module ViewComponent
165
165
  define_method :"with_#{slot_name}" do |collection_args = nil, &block|
166
166
  collection_args.map do |args|
167
167
  if args.respond_to?(:to_hash)
168
- set_slot(slot_name, nil, **args, &block)
168
+ __vc_set_slot(slot_name, nil, **args, &block)
169
169
  else
170
- set_slot(slot_name, nil, *args, &block)
170
+ __vc_set_slot(slot_name, nil, *args, &block)
171
171
  end
172
172
  end
173
173
  end
174
174
 
175
175
  self::GeneratedSlotMethods.define_method slot_name do
176
- get_slot(slot_name)
176
+ __vc_get_slot(slot_name)
177
177
  end
178
178
 
179
179
  self::GeneratedSlotMethods.define_method :"#{slot_name}?" do
180
- get_slot(slot_name).present?
180
+ __vc_get_slot(slot_name).present?
181
181
  end
182
182
 
183
- register_slot(slot_name, collection: true, callable: callable)
183
+ __vc_register_slot(slot_name, collection: true, callable: callable)
184
184
  end
185
185
  end
186
186
 
@@ -211,8 +211,9 @@ module ViewComponent
211
211
  super
212
212
  end
213
213
 
214
+ # @private
214
215
  # Called by the compiler, as instance methods are not defined when slots are first registered
215
- def register_default_slots
216
+ def __vc_register_default_slots
216
217
  registered_slots.each do |slot_name, config|
217
218
  default_method_name = :"default_#{slot_name}"
218
219
  config[:default_method] = instance_methods.find { |method_name| method_name == default_method_name }
@@ -223,17 +224,17 @@ module ViewComponent
223
224
 
224
225
  private
225
226
 
226
- def register_slot(slot_name, **kwargs)
227
- registered_slots[slot_name] = define_slot(slot_name, **kwargs)
227
+ def __vc_register_slot(slot_name, **kwargs)
228
+ registered_slots[slot_name] = __vc_define_slot(slot_name, **kwargs)
228
229
  end
229
230
 
230
- def register_polymorphic_slot(slot_name, types, collection:)
231
+ def __vc_register_polymorphic_slot(slot_name, types, collection:)
231
232
  self::GeneratedSlotMethods.define_method(slot_name) do
232
- get_slot(slot_name)
233
+ __vc_get_slot(slot_name)
233
234
  end
234
235
 
235
236
  self::GeneratedSlotMethods.define_method(:"#{slot_name}?") do
236
- get_slot(slot_name).present?
237
+ __vc_get_slot(slot_name).present?
237
238
  end
238
239
 
239
240
  renderable_hash = types.each_with_object({}) do |(poly_type, poly_attributes_or_callable), memo|
@@ -252,7 +253,7 @@ module ViewComponent
252
253
  "#{slot_name}_#{poly_type}"
253
254
  end
254
255
 
255
- memo[poly_type] = define_slot(
256
+ memo[poly_type] = __vc_define_slot(
256
257
  poly_slot_name, collection: collection, callable: poly_callable
257
258
  )
258
259
 
@@ -263,7 +264,7 @@ module ViewComponent
263
264
  end
264
265
 
265
266
  define_method(setter_method_name) do |*args, **kwargs, &block|
266
- set_polymorphic_slot(slot_name, poly_type, *args, **kwargs, &block)
267
+ __vc_set_polymorphic_slot(slot_name, poly_type, *args, **kwargs, &block)
267
268
  end
268
269
 
269
270
  define_method :"with_#{poly_slot_name}_content" do |content|
@@ -279,7 +280,7 @@ module ViewComponent
279
280
  }
280
281
  end
281
282
 
282
- def define_slot(slot_name, collection:, callable:)
283
+ def __vc_define_slot(slot_name, collection:, callable:)
283
284
  slot = {collection: collection}
284
285
  return slot unless callable
285
286
 
@@ -302,18 +303,18 @@ module ViewComponent
302
303
  slot
303
304
  end
304
305
 
305
- def validate_plural_slot_name(slot_name)
306
+ def __vc_validate_plural_slot_name(slot_name)
306
307
  if RESERVED_NAMES[:plural].include?(slot_name.to_sym)
307
308
  raise ReservedPluralSlotNameError.new(name, slot_name)
308
309
  end
309
310
 
310
- raise_if_slot_name_uncountable(slot_name)
311
- raise_if_slot_conflicts_with_call(slot_name)
312
- raise_if_slot_ends_with_question_mark(slot_name)
313
- raise_if_slot_registered(slot_name)
311
+ __vc_raise_if_slot_name_uncountable(slot_name)
312
+ __vc_raise_if_slot_conflicts_with_call(slot_name)
313
+ __vc_raise_if_slot_ends_with_question_mark(slot_name)
314
+ __vc_raise_if_slot_registered(slot_name)
314
315
  end
315
316
 
316
- def validate_singular_slot_name(slot_name)
317
+ def __vc_validate_singular_slot_name(slot_name)
317
318
  if slot_name.to_sym == :content
318
319
  raise ContentSlotNameError.new(name)
319
320
  end
@@ -322,28 +323,28 @@ module ViewComponent
322
323
  raise ReservedSingularSlotNameError.new(name, slot_name)
323
324
  end
324
325
 
325
- raise_if_slot_conflicts_with_call(slot_name)
326
- raise_if_slot_ends_with_question_mark(slot_name)
327
- raise_if_slot_registered(slot_name)
326
+ __vc_raise_if_slot_conflicts_with_call(slot_name)
327
+ __vc_raise_if_slot_ends_with_question_mark(slot_name)
328
+ __vc_raise_if_slot_registered(slot_name)
328
329
  end
329
330
 
330
- def raise_if_slot_registered(slot_name)
331
+ def __vc_raise_if_slot_registered(slot_name)
331
332
  if registered_slots.key?(slot_name)
332
333
  raise RedefinedSlotError.new(name, slot_name)
333
334
  end
334
335
  end
335
336
 
336
- def raise_if_slot_ends_with_question_mark(slot_name)
337
+ def __vc_raise_if_slot_ends_with_question_mark(slot_name)
337
338
  raise SlotPredicateNameError.new(name, slot_name) if slot_name.to_s.end_with?("?")
338
339
  end
339
340
 
340
- def raise_if_slot_conflicts_with_call(slot_name)
341
+ def __vc_raise_if_slot_conflicts_with_call(slot_name)
341
342
  if slot_name.start_with?("call_")
342
343
  raise InvalidSlotNameError, "Slot cannot start with 'call_'. Please rename #{slot_name}"
343
344
  end
344
345
  end
345
346
 
346
- def raise_if_slot_name_uncountable(slot_name)
347
+ def __vc_raise_if_slot_name_uncountable(slot_name)
347
348
  slot_name = slot_name.to_s
348
349
  if slot_name.pluralize == slot_name.singularize
349
350
  raise UncountableSlotNameError.new(name, slot_name)
@@ -351,9 +352,9 @@ module ViewComponent
351
352
  end
352
353
  end
353
354
 
354
- def get_slot(slot_name)
355
+ def __vc_get_slot(slot_name)
355
356
  @__vc_set_slots ||= {}
356
- content unless content_evaluated? # ensure content is loaded so slots will be defined
357
+ content unless defined?(@__vc_content_evaluated) && @__vc_content_evaluated # ensure content is loaded so slots will be defined
357
358
 
358
359
  # If the slot is set, return it
359
360
  return @__vc_set_slots[slot_name] if @__vc_set_slots[slot_name]
@@ -376,7 +377,7 @@ module ViewComponent
376
377
  end
377
378
  end
378
379
 
379
- def set_slot(slot_name, slot_definition = nil, *args, **kwargs, &block)
380
+ def __vc_set_slot(slot_name, slot_definition = nil, *args, **kwargs, &block)
380
381
  slot_definition ||= self.class.registered_slots[slot_name]
381
382
  slot = Slot.new(self)
382
383
 
@@ -433,7 +434,7 @@ module ViewComponent
433
434
  slot
434
435
  end
435
436
 
436
- def set_polymorphic_slot(slot_name, poly_type = nil, *args, **kwargs, &block)
437
+ def __vc_set_polymorphic_slot(slot_name, poly_type = nil, *args, **kwargs, &block)
437
438
  slot_definition = self.class.registered_slots[slot_name]
438
439
 
439
440
  if !slot_definition[:collection] && defined?(@__vc_set_slots) && @__vc_set_slots[slot_name]
@@ -442,7 +443,7 @@ module ViewComponent
442
443
 
443
444
  poly_def = slot_definition[:renderable_hash][poly_type]
444
445
 
445
- set_slot(slot_name, poly_def, *args, **kwargs, &block)
446
+ __vc_set_slot(slot_name, poly_def, *args, **kwargs, &block)
446
447
  end
447
448
  end
448
449
  end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ViewComponent
4
+ module SystemSpecHelpers
5
+ include SystemTestHelpers
6
+
7
+ def page
8
+ Capybara.current_session
9
+ end
10
+ end
11
+ end
@@ -43,7 +43,7 @@ module ViewComponent
43
43
  attr_reader :source
44
44
 
45
45
  def initialize(component:, inline_template:)
46
- details = ActionView::TemplateDetails.new(nil, inline_template.language.to_sym, nil, nil)
46
+ details = ActionView::TemplateDetails.new(nil, inline_template.language.to_sym, DEFAULT_FORMAT, nil)
47
47
 
48
48
  super(
49
49
  component: component,
@@ -63,7 +63,7 @@ module ViewComponent
63
63
  class InlineCall < Template
64
64
  def initialize(component:, method_name:, defined_on_self:)
65
65
  variant = method_name.to_s.include?("call_") ? method_name.to_s.sub("call_", "").to_sym : nil
66
- details = ActionView::TemplateDetails.new(nil, nil, nil, variant)
66
+ details = ActionView::TemplateDetails.new(nil, nil, DEFAULT_FORMAT, variant)
67
67
 
68
68
  super(component: component, details: details)
69
69
 
@@ -82,7 +82,7 @@ module ViewComponent
82
82
  def safe_method_name_call
83
83
  m = safe_method_name
84
84
  proc do
85
- maybe_escape_html(send(m)) do
85
+ __vc_maybe_escape_html(send(m)) do
86
86
  Kernel.warn("WARNING: The #{self.class} component rendered HTML-unsafe output. " \
87
87
  "The output will be automatically escaped, but you may want to investigate.")
88
88
  end
@@ -98,9 +98,8 @@ module ViewComponent
98
98
  @component.silence_redefinition_of_method(call_method_name)
99
99
 
100
100
  # rubocop:disable Style/EvalWithLocation
101
- @component.class_eval <<~RUBY, @path, @lineno - 1
101
+ @component.class_eval <<~RUBY, @path, @lineno
102
102
  def #{call_method_name}
103
- @view_context.instance_variable_set(:@virtual_path, virtual_path)
104
103
  #{compiled_source}
105
104
  end
106
105
  RUBY
@@ -70,12 +70,12 @@ module ViewComponent
70
70
 
71
71
  def initialize(scope:, load_paths:)
72
72
  @__vc_i18n_scope = scope.split(".").map(&:to_sym)
73
- @load_paths = load_paths
73
+ @__vc_load_paths = load_paths
74
74
  end
75
75
 
76
76
  # Ensure the Simple backend won't load paths from ::I18n.load_path
77
77
  def load_translations
78
- super(@load_paths)
78
+ super(@__vc_load_paths)
79
79
  end
80
80
 
81
81
  def scope_data(data)
@@ -100,7 +100,7 @@ module ViewComponent
100
100
  key = self.class.__vc_i18n_key(key, options.delete(:scope))
101
101
  as_html = HTML_SAFE_TRANSLATION_KEY.match?(key)
102
102
 
103
- html_escape_translation_options!(options) if as_html
103
+ __vc_html_escape_translation_options!(options) if as_html
104
104
 
105
105
  if key.start_with?(__vc_i18n_scope + ".")
106
106
  translated =
@@ -113,7 +113,7 @@ module ViewComponent
113
113
  return @view_context.translate(key, locale: locale, **options)
114
114
  end
115
115
 
116
- translated = html_safe_translation(translated) if as_html
116
+ translated = __vc_html_safe_translation(translated) if as_html
117
117
  translated
118
118
  else
119
119
  @view_context.translate(key, locale: locale, **options)
@@ -127,9 +127,9 @@ module ViewComponent
127
127
 
128
128
  private
129
129
 
130
- def html_safe_translation(translation)
130
+ def __vc_html_safe_translation(translation)
131
131
  if translation.respond_to?(:map)
132
- translation.map { |element| html_safe_translation(element) }
132
+ translation.map { |element| __vc_html_safe_translation(element) }
133
133
  else
134
134
  # It's assumed here that objects loaded by the i18n backend will respond to `#html_safe?`.
135
135
  # It's reasonable that if we're in Rails, `active_support/core_ext/string/output_safety.rb`
@@ -138,7 +138,7 @@ module ViewComponent
138
138
  end
139
139
  end
140
140
 
141
- def html_escape_translation_options!(options)
141
+ def __vc_html_escape_translation_options!(options)
142
142
  options.except(*::I18n::RESERVED_KEYS).each do |name, value|
143
143
  next if name == :count && value.is_a?(Numeric)
144
144
 
@@ -5,7 +5,7 @@ module ViewComponent
5
5
  MAJOR = 4
6
6
  MINOR = 0
7
7
  PATCH = 0
8
- PRE = "alpha6"
8
+ PRE = "rc1"
9
9
 
10
10
  STRING = [MAJOR, MINOR, PATCH, PRE].compact.join(".")
11
11
  end
@@ -7,7 +7,6 @@ module ViewComponent
7
7
  extend ActiveSupport::Autoload
8
8
 
9
9
  autoload :Base
10
- autoload :CaptureCompatibility
11
10
  autoload :Compiler
12
11
  autoload :CompileCache
13
12
  autoload :Config
@@ -15,11 +14,15 @@ module ViewComponent
15
14
  autoload :InlineTemplate
16
15
  autoload :Instrumentation
17
16
  autoload :Preview
18
- autoload :TestHelpers
19
- autoload :SystemTestHelpers
20
- autoload :TestCase
21
- autoload :SystemTestCase
22
17
  autoload :Translatable
18
+
19
+ if Rails.env.test?
20
+ autoload :TestHelpers
21
+ autoload :SystemSpecHelpers
22
+ autoload :SystemTestHelpers
23
+ autoload :TestCase
24
+ autoload :SystemTestCase
25
+ end
23
26
  end
24
27
 
25
28
  require "view_component/engine" if defined?(Rails::Engine)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: view_component
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.0.0.alpha6
4
+ version: 4.0.0.rc1
5
5
  platform: ruby
6
6
  authors:
7
7
  - ViewComponent Team
@@ -54,6 +54,7 @@ files:
54
54
  - app/controllers/view_components_system_test_controller.rb
55
55
  - app/views/test_mailer/test_asset_email.html.erb
56
56
  - app/views/test_mailer/test_email.html.erb
57
+ - app/views/test_mailer/test_url_email.html.erb
57
58
  - app/views/view_components/index.html.erb
58
59
  - app/views/view_components/preview.html.erb
59
60
  - app/views/view_components/previews.html.erb
@@ -74,13 +75,13 @@ files:
74
75
  - lib/view_component/request_details.rb
75
76
  - lib/view_component/slot.rb
76
77
  - lib/view_component/slotable.rb
78
+ - lib/view_component/system_spec_helpers.rb
77
79
  - lib/view_component/system_test_case.rb
78
80
  - lib/view_component/system_test_helpers.rb
79
81
  - lib/view_component/template.rb
80
82
  - lib/view_component/test_case.rb
81
83
  - lib/view_component/test_helpers.rb
82
84
  - lib/view_component/translatable.rb
83
- - lib/view_component/use_helpers.rb
84
85
  - lib/view_component/version.rb
85
86
  - lib/view_component/with_content_helper.rb
86
87
  homepage: https://viewcomponent.org
@@ -88,6 +89,7 @@ licenses:
88
89
  - MIT
89
90
  metadata:
90
91
  allowed_push_host: https://rubygems.org
92
+ rubygems_mfa_required: 'true'
91
93
  source_code_uri: https://github.com/viewcomponent/view_component
92
94
  changelog_uri: https://github.com/ViewComponent/view_component/blob/main/docs/CHANGELOG.md
93
95
  rdoc_options: []
@@ -1,41 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ViewComponent::UseHelpers
4
- extend ActiveSupport::Concern
5
-
6
- class_methods do
7
- def use_helpers(*args, from: nil, prefix: false)
8
- args.each { |helper_method| use_helper(helper_method, from: from, prefix: prefix) }
9
- end
10
-
11
- def use_helper(helper_method, from: nil, prefix: false)
12
- helper_method_name = full_helper_method_name(helper_method, prefix: prefix, source: from)
13
-
14
- class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
15
- def #{helper_method_name}(...)
16
- raise HelpersCalledBeforeRenderError if view_context.nil?
17
-
18
- #{define_helper(helper_method: helper_method, source: from)}
19
- end
20
- RUBY
21
- end
22
-
23
- private
24
-
25
- def full_helper_method_name(helper_method, prefix: false, source: nil)
26
- return helper_method unless prefix.present?
27
-
28
- if !!prefix == prefix
29
- "#{source.to_s.underscore}_#{helper_method}"
30
- else
31
- "#{prefix}_#{helper_method}"
32
- end
33
- end
34
-
35
- def define_helper(helper_method:, source:)
36
- return "__vc_original_view_context.#{helper_method}(...)" unless source.present?
37
-
38
- "#{source}.instance_method(:#{helper_method}).bind(self).call(...)"
39
- end
40
- end
41
- end