view_component 3.23.2 → 4.0.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.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/concerns/view_component/preview_actions.rb +11 -14
  3. data/app/controllers/view_components_system_test_controller.rb +15 -20
  4. data/app/views/test_mailer/test_asset_email.html.erb +1 -0
  5. data/app/views/test_mailer/test_url_email.html.erb +1 -0
  6. data/app/views/view_components/preview.html.erb +1 -9
  7. data/docs/CHANGELOG.md +404 -0
  8. data/lib/{rails/generators → generators/view_component}/abstract_generator.rb +2 -2
  9. data/lib/{rails/generators → generators/view_component}/component/component_generator.rb +16 -3
  10. data/lib/{rails/generators → generators/view_component}/component/templates/component.rb.tt +6 -1
  11. data/lib/{rails/generators/erb/component_generator.rb → generators/view_component/erb/erb_generator.rb} +4 -3
  12. data/lib/{rails/generators/haml/component_generator.rb → generators/view_component/haml/haml_generator.rb} +3 -3
  13. data/lib/{rails/generators/locale/component_generator.rb → generators/view_component/locale/locale_generator.rb} +3 -3
  14. data/lib/{rails/generators/preview/component_generator.rb → generators/view_component/preview/preview_generator.rb} +3 -3
  15. data/lib/{rails/generators/rspec/component_generator.rb → generators/view_component/rspec/rspec_generator.rb} +3 -3
  16. data/lib/{rails/generators/slim/component_generator.rb → generators/view_component/slim/slim_generator.rb} +3 -3
  17. data/lib/{rails/generators/stimulus/component_generator.rb → generators/view_component/stimulus/stimulus_generator.rb} +3 -3
  18. data/lib/generators/view_component/tailwindcss/tailwindcss_generator.rb +11 -0
  19. data/lib/{rails/generators/test_unit/component_generator.rb → generators/view_component/test_unit/test_unit_generator.rb} +2 -2
  20. data/lib/view_component/base.rb +154 -157
  21. data/lib/view_component/collection.rb +11 -25
  22. data/lib/view_component/compiler.rb +52 -79
  23. data/lib/view_component/config.rb +51 -85
  24. data/lib/view_component/configurable.rb +1 -1
  25. data/lib/view_component/deprecation.rb +1 -1
  26. data/lib/view_component/engine.rb +37 -107
  27. data/lib/view_component/errors.rb +16 -34
  28. data/lib/view_component/inline_template.rb +3 -4
  29. data/lib/view_component/instrumentation.rb +4 -10
  30. data/lib/view_component/preview.rb +4 -11
  31. data/lib/view_component/request_details.rb +30 -0
  32. data/lib/view_component/slot.rb +6 -13
  33. data/lib/view_component/slotable.rb +82 -77
  34. data/lib/view_component/system_spec_helpers.rb +11 -0
  35. data/lib/view_component/system_test_helpers.rb +1 -2
  36. data/lib/view_component/template.rb +106 -83
  37. data/lib/view_component/test_helpers.rb +37 -44
  38. data/lib/view_component/translatable.rb +33 -32
  39. data/lib/view_component/version.rb +3 -3
  40. data/lib/view_component.rb +8 -6
  41. metadata +30 -558
  42. data/app/assets/vendor/prism.css +0 -4
  43. data/app/assets/vendor/prism.min.js +0 -12
  44. data/app/helpers/preview_helper.rb +0 -85
  45. data/app/views/view_components/_preview_source.html.erb +0 -17
  46. data/lib/rails/generators/tailwindcss/component_generator.rb +0 -11
  47. data/lib/view_component/capture_compatibility.rb +0 -44
  48. data/lib/view_component/component_error.rb +0 -6
  49. data/lib/view_component/rails/tasks/view_component.rake +0 -20
  50. data/lib/view_component/render_component_helper.rb +0 -10
  51. data/lib/view_component/render_component_to_string_helper.rb +0 -9
  52. data/lib/view_component/render_monkey_patch.rb +0 -13
  53. data/lib/view_component/render_to_string_monkey_patch.rb +0 -13
  54. data/lib/view_component/rendering_component_helper.rb +0 -9
  55. data/lib/view_component/rendering_monkey_patch.rb +0 -13
  56. data/lib/view_component/slotable_default.rb +0 -20
  57. data/lib/view_component/use_helpers.rb +0 -42
  58. /data/lib/{rails/generators → generators/view_component}/erb/templates/component.html.erb.tt +0 -0
  59. /data/lib/{rails/generators → generators/view_component}/haml/templates/component.html.haml.tt +0 -0
  60. /data/lib/{rails/generators → generators/view_component}/preview/templates/component_preview.rb.tt +0 -0
  61. /data/lib/{rails/generators → generators/view_component}/rspec/templates/component_spec.rb.tt +0 -0
  62. /data/lib/{rails/generators → generators/view_component}/slim/templates/component.html.slim.tt +0 -0
  63. /data/lib/{rails/generators → generators/view_component}/stimulus/templates/component_controller.js.tt +0 -0
  64. /data/lib/{rails/generators → generators/view_component}/stimulus/templates/component_controller.ts.tt +0 -0
  65. /data/lib/{rails/generators → generators/view_component}/tailwindcss/templates/component.html.erb.tt +0 -0
  66. /data/lib/{rails/generators → generators/view_component}/test_unit/templates/component_test.rb.tt +0 -0
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "action_view/renderer/collection_renderer" if Rails.version.to_f >= 6.1
3
+ require "action_view/renderer/collection_renderer"
4
4
 
5
5
  module ViewComponent
6
6
  class Collection
@@ -9,25 +9,24 @@ module ViewComponent
9
9
 
10
10
  delegate :size, to: :@collection
11
11
 
12
- attr_accessor :__vc_original_view_context
13
-
14
- def set_original_view_context(view_context)
15
- self.__vc_original_view_context = view_context
16
- end
17
-
18
12
  def render_in(view_context, &block)
19
13
  components.map do |component|
20
- component.set_original_view_context(__vc_original_view_context)
21
14
  component.render_in(view_context, &block)
22
15
  end.join(rendered_spacer(view_context)).html_safe
23
16
  end
24
17
 
18
+ def each(&block)
19
+ components.each(&block)
20
+ end
21
+
22
+ private
23
+
25
24
  def components
26
25
  return @components if defined? @components
27
26
 
28
27
  iterator = ActionView::PartialIteration.new(@collection.size)
29
28
 
30
- component.validate_collection_parameter!(validate_default: true)
29
+ component.__vc_validate_collection_parameter!(validate_default: true)
31
30
 
32
31
  @components = @collection.map do |item|
33
32
  component.new(**component_options(item, iterator)).tap do |component|
@@ -36,18 +35,6 @@ module ViewComponent
36
35
  end
37
36
  end
38
37
 
39
- def each(&block)
40
- components.each(&block)
41
- end
42
-
43
- # Rails expects us to define `format` on all renderables,
44
- # but we do not know the `format` of a ViewComponent until runtime.
45
- def format
46
- nil
47
- end
48
-
49
- private
50
-
51
38
  def initialize(component, object, spacer_component, **options)
52
39
  @component = component
53
40
  @collection = collection_variable(object || [])
@@ -64,16 +51,15 @@ module ViewComponent
64
51
  end
65
52
 
66
53
  def component_options(item, iterator)
67
- item_options = {component.collection_parameter => item}
68
- item_options[component.collection_counter_parameter] = iterator.index if component.counter_argument_present?
69
- item_options[component.collection_iteration_parameter] = iterator.dup if component.iteration_argument_present?
54
+ item_options = {component.__vc_collection_parameter => item}
55
+ item_options[component.__vc_collection_counter_parameter] = iterator.index if component.__vc_counter_argument_present?
56
+ item_options[component.__vc_collection_iteration_parameter] = iterator.dup if component.__vc_iteration_argument_present?
70
57
 
71
58
  @options.merge(item_options)
72
59
  end
73
60
 
74
61
  def rendered_spacer(view_context)
75
62
  if @spacer_component
76
- @spacer_component.set_original_view_context(__vc_original_view_context)
77
63
  @spacer_component.render_in(view_context)
78
64
  else
79
65
  ""
@@ -8,7 +8,7 @@ module ViewComponent
8
8
  # * true (a blocking mode which ensures thread safety when redefining the `call` method for components,
9
9
  # default in Rails development and test mode)
10
10
  # * false(a non-blocking mode, default in Rails production mode)
11
- class_attribute :development_mode, default: false
11
+ class_attribute :__vc_development_mode, default: false
12
12
 
13
13
  def initialize(component)
14
14
  @component = component
@@ -30,8 +30,8 @@ module ViewComponent
30
30
 
31
31
  gather_templates
32
32
 
33
- if self.class.development_mode && @templates.any?(&:requires_compiled_superclass?)
34
- @component.superclass.compile(raise_errors: raise_errors)
33
+ if self.class.__vc_development_mode && @templates.any?(&:requires_compiled_superclass?)
34
+ @component.superclass.__vc_compile(raise_errors: raise_errors)
35
35
  end
36
36
 
37
37
  if template_errors.present?
@@ -42,19 +42,36 @@ module ViewComponent
42
42
  end
43
43
 
44
44
  if raise_errors
45
- @component.validate_initialization_parameters!
46
- @component.validate_collection_parameter!
45
+ @component.__vc_validate_initialization_parameters!
46
+ @component.__vc_validate_collection_parameter!
47
47
  end
48
48
 
49
49
  define_render_template_for
50
50
 
51
- @component.register_default_slots
52
- @component.build_i18n_backend
51
+ @component.__vc_register_default_slots
52
+ @component.__vc_build_i18n_backend
53
53
 
54
54
  CompileCache.register(@component)
55
55
  end
56
56
  end
57
57
 
58
+ # @return all matching compiled templates, in priority order based on the requested details from LookupContext
59
+ #
60
+ # @param [ActionView::TemplateDetails::Requested] requested_details i.e. locales, formats, variants
61
+ def find_templates_for(requested_details)
62
+ filtered_templates = @templates.select do |template|
63
+ template.details.matches?(requested_details)
64
+ end
65
+
66
+ if filtered_templates.count > 1
67
+ filtered_templates.sort_by! do |template|
68
+ template.details.sort_key_for(requested_details)
69
+ end
70
+ end
71
+
72
+ filtered_templates
73
+ end
74
+
58
75
  private
59
76
 
60
77
  attr_reader :templates
@@ -64,40 +81,25 @@ module ViewComponent
64
81
  template.compile_to_component
65
82
  end
66
83
 
67
- method_body =
68
- if @templates.one?
69
- @templates.first.safe_method_name_call
70
- elsif (template = @templates.find(&:inline?))
71
- template.safe_method_name_call
72
- else
73
- branches = []
74
-
75
- @templates.each do |template|
76
- conditional =
77
- if template.inline_call?
78
- "variant&.to_sym == #{template.variant.inspect}"
79
- else
80
- [
81
- template.default_format? ? "(format == #{ViewComponent::Base::VC_INTERNAL_DEFAULT_FORMAT.inspect} || format.nil?)" : "format == #{template.format.inspect}",
82
- template.variant.nil? ? "variant.nil?" : "variant&.to_sym == #{template.variant.inspect}"
83
- ].join(" && ")
84
- end
85
-
86
- branches << [conditional, template.safe_method_name_call]
87
- end
84
+ @component.silence_redefinition_of_method(:render_template_for)
88
85
 
89
- out = branches.each_with_object(+"") do |(conditional, branch_body), memo|
90
- memo << "#{(!memo.present?) ? "if" : "elsif"} #{conditional}\n #{branch_body}\n"
86
+ if @templates.one?
87
+ template = @templates.first
88
+ safe_call = template.safe_method_name_call
89
+ @component.define_method(:render_template_for) do |_|
90
+ @current_template = template
91
+ instance_exec(&safe_call)
92
+ end
93
+ else
94
+ compiler = self
95
+ @component.define_method(:render_template_for) do |details|
96
+ if (@current_template = compiler.find_templates_for(details).first)
97
+ instance_exec(&@current_template.safe_method_name_call)
98
+ else
99
+ raise MissingTemplateError.new(self.class.name, details)
91
100
  end
92
- out << "else\n #{templates.find { _1.variant.nil? && _1.default_format? }.safe_method_name_call}\nend"
93
101
  end
94
-
95
- @component.silence_redefinition_of_method(:render_template_for)
96
- @component.class_eval <<-RUBY, __FILE__, __LINE__ + 1
97
- def render_template_for(variant = nil, format = nil)
98
- #{method_body}
99
102
  end
100
- RUBY
101
103
  end
102
104
 
103
105
  def template_errors
@@ -106,10 +108,7 @@ module ViewComponent
106
108
 
107
109
  errors << "Couldn't find a template file or inline render method for #{@component}." if @templates.empty?
108
110
 
109
- # We currently allow components to have both an inline call method and a template for a variant, with the
110
- # inline call method overriding the template. We should aim to change this in v4 to instead
111
- # raise an error.
112
- @templates.reject(&:inline_call?)
111
+ @templates
113
112
  .map { |template| [template.variant, template.format] }
114
113
  .tally
115
114
  .select { |_, count| count > 1 }
@@ -168,30 +167,18 @@ module ViewComponent
168
167
 
169
168
  def gather_templates
170
169
  @templates ||=
171
- begin
170
+ if @component.__vc_inline_template.present?
171
+ [Template::Inline.new(
172
+ component: @component,
173
+ inline_template: @component.__vc_inline_template
174
+ )]
175
+ else
176
+ path_parser = ActionView::Resolver::PathParser.new
172
177
  templates = @component.sidecar_files(
173
178
  ActionView::Template.template_handler_extensions
174
179
  ).map do |path|
175
- # Extract format and variant from template filename
176
- this_format, variant =
177
- File
178
- .basename(path) # "variants_component.html+mini.watch.erb"
179
- .split(".")[1..-2] # ["html+mini", "watch"]
180
- .join(".") # "html+mini.watch"
181
- .split("+") # ["html", "mini.watch"]
182
- .map(&:to_sym) # [:html, :"mini.watch"]
183
-
184
- out = Template.new(
185
- component: @component,
186
- type: :file,
187
- path: path,
188
- lineno: 0,
189
- extension: path.split(".").last,
190
- this_format: this_format.to_s.split(".").last&.to_sym, # strip locale from this_format, see #2113
191
- variant: variant
192
- )
193
-
194
- out
180
+ details = path_parser.parse(path).details
181
+ Template::File.new(component: @component, path: path, details: details)
195
182
  end
196
183
 
197
184
  component_instance_methods_on_self = @component.instance_methods(false)
@@ -201,24 +188,10 @@ module ViewComponent
201
188
  ).flat_map { |ancestor| ancestor.instance_methods(false).grep(/^call(_|$)/) }
202
189
  .uniq
203
190
  .each do |method_name|
204
- templates << Template.new(
205
- component: @component,
206
- type: :inline_call,
207
- this_format: ViewComponent::Base::VC_INTERNAL_DEFAULT_FORMAT,
208
- variant: method_name.to_s.include?("call_") ? method_name.to_s.sub("call_", "").to_sym : nil,
209
- method_name: method_name,
210
- defined_on_self: component_instance_methods_on_self.include?(method_name)
211
- )
212
- end
213
-
214
- if @component.inline_template.present?
215
- templates << Template.new(
191
+ templates << Template::InlineCall.new(
216
192
  component: @component,
217
- type: :inline,
218
- path: @component.inline_template.path,
219
- lineno: @component.inline_template.lineno,
220
- source: @component.inline_template.source.dup,
221
- extension: @component.inline_template.language
193
+ method_name: method_name,
194
+ defined_on_self: component_instance_methods_on_self.include?(method_name)
222
195
  )
223
196
  end
224
197
 
@@ -13,19 +13,8 @@ module ViewComponent
13
13
  def defaults
14
14
  ActiveSupport::OrderedOptions.new.merge!({
15
15
  generate: default_generate_options,
16
- preview_controller: "ViewComponentsController",
17
- preview_route: "/rails/view_components",
18
- show_previews_source: false,
19
- instrumentation_enabled: false,
20
- use_deprecated_instrumentation_name: true,
21
- render_monkey_patch_enabled: true,
22
- view_component_path: "app/components",
23
- component_parent_class: nil,
24
- show_previews: Rails.env.development? || Rails.env.test?,
25
- preview_paths: default_preview_paths,
26
- test_controller: "ApplicationController",
27
- default_preview_layout: nil,
28
- capture_compatibility_patch_enabled: false
16
+ previews: default_previews_options,
17
+ instrumentation_enabled: false
29
18
  })
30
19
  end
31
20
 
@@ -36,6 +25,12 @@ module ViewComponent
36
25
  # All options under this namespace default to `false` unless otherwise
37
26
  # stated.
38
27
  #
28
+ # #### `#path`
29
+ #
30
+ # Where to put generated components. Defaults to `app/components`:
31
+ #
32
+ # config.view_component.generate.path = "lib/components"
33
+ #
39
34
  # #### `#sidecar`
40
35
  #
41
36
  # Always generate a component with a sidecar directory:
@@ -83,96 +78,56 @@ module ViewComponent
83
78
  #
84
79
  # Required when there is more than one path defined in preview_paths.
85
80
  # Defaults to `""`. If this is blank, the generator will use
86
- # `ViewComponent.config.preview_paths` if defined,
81
+ # `ViewComponent.config.previews.paths` if defined,
87
82
  # `"test/components/previews"` otherwise
88
83
  #
89
84
  # #### `#use_component_path_for_rspec_tests`
90
85
  #
91
- # Whether to use the `config.view_component_path` when generating new
86
+ # Whether to use `config.generate.path` when generating new
92
87
  # RSpec component tests:
93
88
  #
94
89
  # config.view_component.generate.use_component_path_for_rspec_tests = true
95
90
  #
96
- # When set to `true`, the generator will use the `view_component_path` to
91
+ # When set to `true`, the generator will use the `path` to
97
92
  # decide where to generate the new RSpec component test.
98
- # For example, if the `view_component_path` is
93
+ # For example, if the `path` is
99
94
  # `app/views/components`, then the generator will create a new spec file
100
95
  # in `spec/views/components/` rather than the default `spec/components/`.
101
96
 
102
- # @!attribute preview_controller
103
- # @return [String]
104
- # The controller used for previewing components.
105
- # Defaults to `ViewComponentsController`.
106
-
107
- # @!attribute preview_route
108
- # @return [String]
109
- # The entry route for component previews.
110
- # Defaults to `"/rails/view_components"`.
111
-
112
- # @!attribute show_previews_source
113
- # @return [Boolean]
114
- # Whether to display source code previews in component previews.
115
- # Defaults to `false`.
97
+ # @!attribute previews
98
+ # @return [ActiveSupport::OrderedOptions]
99
+ # The subset of configuration options relating to previews.
100
+ #
101
+ # #### `#controller`
102
+ #
103
+ # The controller used for previewing components. Defaults to `ViewComponentsController`:
104
+ #
105
+ # config.view_component.previews.controller = "MyPreviewController"
106
+ #
107
+ # #### `#route`
108
+ #
109
+ # The entry route for component previews. Defaults to `/rails/view_components`:
110
+ #
111
+ # config.view_component.previews.route = "/my_previews"
112
+ #
113
+ # #### `#enabled`
114
+ #
115
+ # Whether component previews are enabled. Defaults to `true` in development and test environments:
116
+ #
117
+ # config.view_component.previews.enabled = false
118
+ #
119
+ # #### `#default_layout`
120
+ #
121
+ # A custom default layout used for the previews index page and individual previews. Defaults to `nil`:
122
+ #
123
+ # config.view_component.previews.default_layout = "preview_layout"
124
+ #
116
125
 
117
126
  # @!attribute instrumentation_enabled
118
127
  # @return [Boolean]
119
128
  # Whether ActiveSupport notifications are enabled.
120
129
  # Defaults to `false`.
121
130
 
122
- # @!attribute use_deprecated_instrumentation_name
123
- # @return [Boolean]
124
- # Whether ActiveSupport Notifications use the private name `"!render.view_component"`
125
- # or are made more publicly available via `"render.view_component"`.
126
- # Will be removed in next major version.
127
- # Defaults to `true`.
128
-
129
- # @!attribute render_monkey_patch_enabled
130
- # @return [Boolean] Whether the #render method should be monkey patched.
131
- # If this is disabled, use `#render_component` or
132
- # `#render_component_to_string` instead.
133
- # Defaults to `true`.
134
-
135
- # @!attribute view_component_path
136
- # @return [String]
137
- # The path in which components, their templates, and their sidecars should
138
- # be stored.
139
- # Defaults to `"app/components"`.
140
-
141
- # @!attribute component_parent_class
142
- # @return [String]
143
- # The parent class from which generated components will inherit.
144
- # Defaults to `nil`. If this is falsy, generators will use
145
- # `"ApplicationComponent"` if defined, `"ViewComponent::Base"` otherwise.
146
-
147
- # @!attribute show_previews
148
- # @return [Boolean]
149
- # Whether component previews are enabled.
150
- # Defaults to `true` in development and test environments.
151
-
152
- # @!attribute preview_paths
153
- # @return [Array<String>]
154
- # The locations in which component previews will be looked up.
155
- # Defaults to `['test/components/previews']` relative to your Rails root.
156
-
157
- # @!attribute test_controller
158
- # @return [String]
159
- # The controller used for testing components.
160
- # Can also be configured on a per-test basis using `#with_controller_class`.
161
- # Defaults to `ApplicationController`.
162
-
163
- # @!attribute default_preview_layout
164
- # @return [String]
165
- # A custom default layout used for the previews index page and individual
166
- # previews.
167
- # Defaults to `nil`. If this is falsy, `"component_preview"` is used.
168
-
169
- # @!attribute capture_compatibility_patch_enabled
170
- # @return [Boolean]
171
- # Enables the experimental capture compatibility patch that makes ViewComponent
172
- # compatible with forms, capture, and other built-ins.
173
- # previews.
174
- # Defaults to `false`.
175
-
176
131
  def default_preview_paths
177
132
  (default_rails_preview_paths + default_rails_engines_preview_paths).uniq
178
133
  end
@@ -200,6 +155,17 @@ module ViewComponent
200
155
  def default_generate_options
201
156
  options = ActiveSupport::OrderedOptions.new(false)
202
157
  options.preview_path = ""
158
+ options.path = "app/components"
159
+ options
160
+ end
161
+
162
+ def default_previews_options
163
+ options = ActiveSupport::OrderedOptions.new
164
+ options.controller = "ViewComponentsController"
165
+ options.route = "/rails/view_components"
166
+ options.enabled = Rails.env.development? || Rails.env.test?
167
+ options.default_layout = nil
168
+ options.paths = default_preview_paths
203
169
  options
204
170
  end
205
171
  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
 
@@ -3,6 +3,6 @@
3
3
  require "active_support/deprecation"
4
4
 
5
5
  module ViewComponent
6
- DEPRECATION_HORIZON = "4.0.0"
6
+ DEPRECATION_HORIZON = "5.0.0"
7
7
  Deprecation = ActiveSupport::Deprecation.new(DEPRECATION_HORIZON, "ViewComponent")
8
8
  end