view_component 2.33.0 → 2.37.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.

Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/app/assets/vendor/prism.css +196 -0
  4. data/app/assets/vendor/prism.min.js +12 -0
  5. data/app/controllers/view_components_controller.rb +1 -1
  6. data/app/helpers/preview_helper.rb +19 -0
  7. data/app/views/test_mailer/test_email.html.erb +1 -0
  8. data/app/views/view_components/_preview_source.html.erb +17 -0
  9. data/app/views/view_components/preview.html.erb +6 -2
  10. data/{CHANGELOG.md → docs/CHANGELOG.md} +151 -1
  11. data/lib/rails/generators/abstract_generator.rb +29 -0
  12. data/lib/rails/generators/component/component_generator.rb +5 -5
  13. data/lib/rails/generators/erb/component_generator.rb +7 -16
  14. data/lib/rails/generators/haml/component_generator.rb +6 -16
  15. data/lib/rails/generators/slim/component_generator.rb +6 -16
  16. data/lib/view_component.rb +2 -0
  17. data/lib/view_component/base.rb +144 -85
  18. data/lib/view_component/collection.rb +6 -2
  19. data/lib/view_component/compile_cache.rb +1 -0
  20. data/lib/view_component/compiler.rb +87 -53
  21. data/lib/view_component/content_areas.rb +57 -0
  22. data/lib/view_component/engine.rb +31 -3
  23. data/lib/view_component/instrumentation.rb +21 -0
  24. data/lib/view_component/preview.rb +19 -8
  25. data/lib/view_component/previewable.rb +16 -18
  26. data/lib/view_component/slot_v2.rb +34 -27
  27. data/lib/view_component/slotable.rb +2 -1
  28. data/lib/view_component/slotable_v2.rb +58 -24
  29. data/lib/view_component/test_helpers.rb +7 -1
  30. data/lib/view_component/translatable.rb +6 -5
  31. data/lib/view_component/version.rb +1 -1
  32. data/lib/view_component/with_content_helper.rb +5 -2
  33. data/lib/yard/mattr_accessor_handler.rb +19 -0
  34. metadata +76 -39
@@ -5,6 +5,7 @@ require "action_view/renderer/collection_renderer" if Rails.version.to_f >= 6.1
5
5
  module ViewComponent
6
6
  class Collection
7
7
  attr_reader :component
8
+
8
9
  delegate :format, to: :component
9
10
 
10
11
  def render_in(view_context, &block)
@@ -16,7 +17,7 @@ module ViewComponent
16
17
  content = component.new(**component_options(item, iterator)).render_in(view_context, &block)
17
18
  iterator.iterate!
18
19
  content
19
- end.join.html_safe
20
+ end.join.html_safe # rubocop:disable Rails/OutputSafety
20
21
  end
21
22
 
22
23
  private
@@ -31,7 +32,10 @@ module ViewComponent
31
32
  if object.respond_to?(:to_ary)
32
33
  object.to_ary
33
34
  else
34
- raise ArgumentError.new("The value of the argument isn't a valid collection. Make sure it responds to to_ary: #{object.inspect}")
35
+ raise ArgumentError.new(
36
+ "The value of the first argument passed to `with_collection` isn't a valid collection. " \
37
+ "Make sure it responds to `to_ary`."
38
+ )
35
39
  end
36
40
  end
37
41
 
@@ -7,6 +7,7 @@ module ViewComponent
7
7
  mattr_accessor :cache, instance_reader: false, instance_accessor: false do
8
8
  Set.new
9
9
  end
10
+
10
11
  module_function
11
12
 
12
13
  def register(klass)
@@ -16,17 +16,22 @@ module ViewComponent
16
16
  subclass_instance_methods = component_class.instance_methods(false)
17
17
 
18
18
  if subclass_instance_methods.include?(:with_content) && raise_errors
19
- raise ViewComponent::ComponentError.new("#{component_class} implements a reserved method, `with_content`.")
19
+ raise ViewComponent::ComponentError.new(
20
+ "#{component_class} implements a reserved method, `#with_content`.\n\n" \
21
+ "To fix this issue, change the name of the method."
22
+ )
20
23
  end
21
24
 
22
25
  if template_errors.present?
23
26
  raise ViewComponent::TemplateError.new(template_errors) if raise_errors
27
+
24
28
  return false
25
29
  end
26
30
 
27
31
  if subclass_instance_methods.include?(:before_render_check)
28
32
  ActiveSupport::Deprecation.warn(
29
- "`before_render_check` will be removed in v3.0.0. Use `before_render` instead."
33
+ "`#before_render_check` will be removed in v3.0.0.\n\n" \
34
+ "To fix this issue, use `#before_render` instead."
30
35
  )
31
36
  end
32
37
 
@@ -39,7 +44,10 @@ module ViewComponent
39
44
  # Remove existing compiled template methods,
40
45
  # as Ruby warns when redefining a method.
41
46
  method_name = call_method_name(template[:variant])
42
- component_class.send(:undef_method, method_name.to_sym) if component_class.instance_methods.include?(method_name.to_sym)
47
+
48
+ if component_class.instance_methods.include?(method_name.to_sym)
49
+ component_class.send(:undef_method, method_name.to_sym)
50
+ end
43
51
 
44
52
  component_class.class_eval <<-RUBY, template[:path], -1
45
53
  def #{method_name}
@@ -61,7 +69,9 @@ module ViewComponent
61
69
  attr_reader :component_class
62
70
 
63
71
  def define_render_template_for
64
- component_class.send(:undef_method, :render_template_for) if component_class.instance_methods.include?(:render_template_for)
72
+ if component_class.instance_methods.include?(:render_template_for)
73
+ component_class.send(:undef_method, :render_template_for)
74
+ end
65
75
 
66
76
  variant_elsifs = variants.compact.uniq.map do |variant|
67
77
  "elsif variant.to_sym == :#{variant}\n #{call_method_name(variant)}"
@@ -77,72 +87,90 @@ module ViewComponent
77
87
  end
78
88
  end
79
89
  RUBY
80
-
81
90
  end
82
91
 
83
92
  def template_errors
84
- @_template_errors ||= begin
85
- errors = []
93
+ @__vc_template_errors ||=
94
+ begin
95
+ errors = []
86
96
 
87
- if (templates + inline_calls).empty?
88
- errors << "Could not find a template file or inline render method for #{component_class}."
89
- end
97
+ if (templates + inline_calls).empty?
98
+ errors << "Could not find a template file or inline render method for #{component_class}."
99
+ end
90
100
 
91
- if templates.count { |template| template[:variant].nil? } > 1
92
- errors << "More than one template found for #{component_class}. There can only be one default template file per component."
93
- end
101
+ if templates.count { |template| template[:variant].nil? } > 1
102
+ errors <<
103
+ "More than one template found for #{component_class}. " \
104
+ "There can only be one default template file per component."
105
+ end
94
106
 
95
- invalid_variants = templates
96
- .group_by { |template| template[:variant] }
97
- .map { |variant, grouped| variant if grouped.length > 1 }
98
- .compact
99
- .sort
107
+ invalid_variants =
108
+ templates.
109
+ group_by { |template| template[:variant] }.
110
+ map { |variant, grouped| variant if grouped.length > 1 }.
111
+ compact.
112
+ sort
113
+
114
+ unless invalid_variants.empty?
115
+ errors <<
116
+ "More than one template found for #{'variant'.pluralize(invalid_variants.count)} " \
117
+ "#{invalid_variants.map { |v| "'#{v}'" }.to_sentence} in #{component_class}. " \
118
+ "There can only be one template file per variant."
119
+ end
100
120
 
101
- unless invalid_variants.empty?
102
- errors << "More than one template found for #{'variant'.pluralize(invalid_variants.count)} #{invalid_variants.map { |v| "'#{v}'" }.to_sentence} in #{component_class}. There can only be one template file per variant."
103
- end
121
+ if templates.find { |template| template[:variant].nil? } && inline_calls_defined_on_self.include?(:call)
122
+ errors <<
123
+ "Template file and inline render method found for #{component_class}. " \
124
+ "There can only be a template file or inline render method per component."
125
+ end
104
126
 
105
- if templates.find { |template| template[:variant].nil? } && inline_calls_defined_on_self.include?(:call)
106
- errors << "Template file and inline render method found for #{component_class}. There can only be a template file or inline render method per component."
107
- end
127
+ duplicate_template_file_and_inline_variant_calls =
128
+ templates.pluck(:variant) & variants_from_inline_calls(inline_calls_defined_on_self)
108
129
 
109
- duplicate_template_file_and_inline_variant_calls =
110
- templates.pluck(:variant) & variants_from_inline_calls(inline_calls_defined_on_self)
130
+ unless duplicate_template_file_and_inline_variant_calls.empty?
131
+ count = duplicate_template_file_and_inline_variant_calls.count
111
132
 
112
- unless duplicate_template_file_and_inline_variant_calls.empty?
113
- count = duplicate_template_file_and_inline_variant_calls.count
133
+ errors <<
134
+ "Template #{'file'.pluralize(count)} and inline render #{'method'.pluralize(count)} " \
135
+ "found for #{'variant'.pluralize(count)} " \
136
+ "#{duplicate_template_file_and_inline_variant_calls.map { |v| "'#{v}'" }.to_sentence} " \
137
+ "in #{component_class}. " \
138
+ "There can only be a template file or inline render method per variant."
139
+ end
114
140
 
115
- errors << "Template #{'file'.pluralize(count)} and inline render #{'method'.pluralize(count)} found for #{'variant'.pluralize(count)} #{duplicate_template_file_and_inline_variant_calls.map { |v| "'#{v}'" }.to_sentence} in #{component_class}. There can only be a template file or inline render method per variant."
141
+ errors
116
142
  end
117
-
118
- errors
119
- end
120
143
  end
121
144
 
122
145
  def templates
123
- @templates ||= begin
124
- extensions = ActionView::Template.template_handler_extensions
125
-
126
- component_class._sidecar_files(extensions).each_with_object([]) do |path, memo|
127
- pieces = File.basename(path).split(".")
128
- memo << {
129
- path: path,
130
- variant: pieces.second.split("+").second&.to_sym,
131
- handler: pieces.last
132
- }
146
+ @templates ||=
147
+ begin
148
+ extensions = ActionView::Template.template_handler_extensions
149
+
150
+ component_class._sidecar_files(extensions).each_with_object([]) do |path, memo|
151
+ pieces = File.basename(path).split(".")
152
+ memo << {
153
+ path: path,
154
+ variant: pieces.second.split("+").second&.to_sym,
155
+ handler: pieces.last
156
+ }
157
+ end
133
158
  end
134
- end
135
159
  end
136
160
 
137
161
  def inline_calls
138
- @inline_calls ||= begin
139
- # Fetch only ViewComponent ancestor classes to limit the scope of
140
- # finding inline calls
141
- view_component_ancestors =
142
- component_class.ancestors.take_while { |ancestor| ancestor != ViewComponent::Base } - component_class.included_modules
143
-
144
- view_component_ancestors.flat_map { |ancestor| ancestor.instance_methods(false).grep(/^call/) }.uniq
145
- end
162
+ @inline_calls ||=
163
+ begin
164
+ # Fetch only ViewComponent ancestor classes to limit the scope of
165
+ # finding inline calls
166
+ view_component_ancestors =
167
+ (
168
+ component_class.ancestors.take_while { |ancestor| ancestor != ViewComponent::Base } -
169
+ component_class.included_modules
170
+ )
171
+
172
+ view_component_ancestors.flat_map { |ancestor| ancestor.instance_methods(false).grep(/^call/) }.uniq
173
+ end
146
174
  end
147
175
 
148
176
  def inline_calls_defined_on_self
@@ -150,7 +178,7 @@ module ViewComponent
150
178
  end
151
179
 
152
180
  def variants
153
- @_variants = (
181
+ @__vc_variants = (
154
182
  templates.map { |template| template[:variant] } + variants_from_inline_calls(inline_calls)
155
183
  ).compact.uniq
156
184
  end
@@ -168,7 +196,13 @@ module ViewComponent
168
196
  if handler.method(:call).parameters.length > 1
169
197
  handler.call(component_class, template)
170
198
  else
171
- handler.call(OpenStruct.new(source: template, identifier: component_class.identifier, type: component_class.type))
199
+ handler.call(
200
+ OpenStruct.new(
201
+ source: template,
202
+ identifier: component_class.identifier,
203
+ type: component_class.type
204
+ )
205
+ )
172
206
  end
173
207
  end
174
208
 
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/concern"
4
+
5
+ require "view_component/slot"
6
+
7
+ # DEPRECATED - ContentAreas is deprecated and will be removed in v3.0.0
8
+ module ViewComponent
9
+ module ContentAreas
10
+ extend ActiveSupport::Concern
11
+
12
+ # Assign the provided content to the content area accessor
13
+ #
14
+ # @private
15
+ def with(area, content = nil, &block)
16
+ unless content_areas.include?(area)
17
+ raise ArgumentError.new(
18
+ "Unknown content_area '#{area}' for #{self} - expected one of '#{content_areas}'.\n\n" \
19
+ "To fix this issue, add `with_content_area :#{area}` to #{self} or reference " \
20
+ "a valid content area."
21
+ )
22
+ end
23
+
24
+ if block_given?
25
+ content = view_context.capture(&block)
26
+ end
27
+
28
+ instance_variable_set("@#{area}".to_sym, content)
29
+ nil
30
+ end
31
+
32
+ class_methods do
33
+ def with_content_areas(*areas)
34
+ ActiveSupport::Deprecation.warn(
35
+ "`with_content_areas` is deprecated and will be removed in ViewComponent v3.0.0.\n\n" \
36
+ "Use slots (https://viewcomponent.org/guide/slots.html) instead."
37
+ )
38
+
39
+ if areas.include?(:content)
40
+ raise ArgumentError.new(
41
+ "#{self} defines a content area called :content, which is a reserved name. \n\n" \
42
+ "To fix this issue, use another name, such as `:body`."
43
+ )
44
+ end
45
+
46
+ areas.each do |area|
47
+ define_method area.to_sym do
48
+ content unless content_evaluated? # ensure content is loaded so content_areas will be defined
49
+ instance_variable_get(:"@#{area}") if instance_variable_defined?(:"@#{area}")
50
+ end
51
+ end
52
+
53
+ self.content_areas = areas
54
+ end
55
+ end
56
+ end
57
+ end
@@ -13,6 +13,7 @@ module ViewComponent
13
13
 
14
14
  options.render_monkey_patch_enabled = true if options.render_monkey_patch_enabled.nil?
15
15
  options.show_previews = Rails.env.development? || Rails.env.test? if options.show_previews.nil?
16
+ options.instrumentation_enabled = false if options.instrumentation_enabled.nil?
16
17
  options.preview_route ||= ViewComponent::Base.preview_route
17
18
  options.preview_controller ||= ViewComponent::Base.preview_controller
18
19
 
@@ -30,7 +31,17 @@ module ViewComponent
30
31
  end
31
32
 
32
33
  ActiveSupport.on_load(:view_component) do
33
- options.each { |k, v| send("#{k}=", v) }
34
+ options.each { |k, v| send("#{k}=", v) if respond_to?("#{k}=") }
35
+ end
36
+ end
37
+
38
+ initializer "view_component.enable_instrumentation" do |app|
39
+ ActiveSupport.on_load(:view_component) do
40
+ if app.config.view_component.instrumentation_enabled.present?
41
+ # :nocov:
42
+ ViewComponent::Base.prepend(ViewComponent::Instrumentation)
43
+ # :nocov:
44
+ end
34
45
  end
35
46
  end
36
47
 
@@ -86,6 +97,12 @@ module ViewComponent
86
97
  end
87
98
  end
88
99
 
100
+ initializer "static assets" do |app|
101
+ if app.config.view_component.show_previews
102
+ app.middleware.insert_before(::ActionDispatch::Static, ::ActionDispatch::Static, "#{root}/app/assets/vendor")
103
+ end
104
+ end
105
+
89
106
  config.after_initialize do |app|
90
107
  options = app.config.view_component
91
108
 
@@ -93,8 +110,19 @@ module ViewComponent
93
110
  app.routes.prepend do
94
111
  preview_controller = options.preview_controller.sub(/Controller$/, "").underscore
95
112
 
96
- get options.preview_route, to: "#{preview_controller}#index", as: :preview_view_components, internal: true
97
- get "#{options.preview_route}/*path", to: "#{preview_controller}#previews", as: :preview_view_component, internal: true
113
+ get(
114
+ options.preview_route,
115
+ to: "#{preview_controller}#index",
116
+ as: :preview_view_components,
117
+ internal: true
118
+ )
119
+
120
+ get(
121
+ "#{options.preview_route}/*path",
122
+ to: "#{preview_controller}#previews",
123
+ as: :preview_view_component,
124
+ internal: true
125
+ )
98
126
  end
99
127
  end
100
128
 
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/notifications"
4
+
5
+ module ViewComponent # :nodoc:
6
+ module Instrumentation
7
+ def self.included(mod)
8
+ mod.prepend(self)
9
+ end
10
+
11
+ def render_in(view_context, &block)
12
+ ActiveSupport::Notifications.instrument(
13
+ "!render.view_component",
14
+ name: self.class.name,
15
+ identifier: self.class.identifier
16
+ ) do
17
+ super(view_context, &block)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -71,19 +71,30 @@ module ViewComponent # :nodoc:
71
71
 
72
72
  # Returns the relative path (from preview_path) to the preview example template if the template exists
73
73
  def preview_example_template_path(example)
74
- preview_path = Array(preview_paths).detect do |preview_path|
75
- Dir["#{preview_path}/#{preview_name}_preview/#{example}.html.*"].first
76
- end
74
+ preview_path =
75
+ Array(preview_paths).detect do |preview_path|
76
+ Dir["#{preview_path}/#{preview_name}_preview/#{example}.html.*"].first
77
+ end
77
78
 
78
79
  if preview_path.nil?
79
- raise PreviewTemplateError, "preview template for example #{example} does not exist"
80
+ raise(
81
+ PreviewTemplateError,
82
+ "A preview template for example #{example} does not exist.\n\n" \
83
+ "To fix this issue, create a template for the example."
84
+ )
80
85
  end
81
86
 
82
87
  path = Dir["#{preview_path}/#{preview_name}_preview/#{example}.html.*"].first
83
- Pathname.new(path)
84
- .relative_path_from(Pathname.new(preview_path))
85
- .to_s
86
- .sub(/\..*$/, "")
88
+ Pathname.new(path).
89
+ relative_path_from(Pathname.new(preview_path)).
90
+ to_s.
91
+ sub(/\..*$/, "")
92
+ end
93
+
94
+ # Returns the method body for the example from the preview file.
95
+ def preview_source(example)
96
+ source = self.instance_method(example.to_sym).source.split("\n")
97
+ source[1...(source.size - 1)].join("\n")
87
98
  end
88
99
 
89
100
  private
@@ -2,51 +2,49 @@
2
2
 
3
3
  require "active_support/concern"
4
4
 
5
- module ViewComponent # :nodoc:
5
+ module ViewComponent
6
6
  module Previewable
7
7
  extend ActiveSupport::Concern
8
8
 
9
9
  included do
10
- # Set a custom default preview layout through app configuration:
10
+ # Enable or disable component previews:
11
11
  #
12
- # config.view_component.default_preview_layout = "component_preview"
12
+ # config.view_component.show_previews = true
13
13
  #
14
- # This affects preview index pages as well as individual component previews
14
+ # Defaults to `true` in development.
15
+ #
16
+ mattr_accessor :show_previews, instance_writer: false
17
+
18
+ # Set a custom default layout used for preview index and individual previews:
19
+ #
20
+ # config.view_component.default_preview_layout = "component_preview"
15
21
  #
16
22
  mattr_accessor :default_preview_layout, instance_writer: false
17
23
 
18
- # Set the location of component previews through app configuration:
24
+ # Set the location of component previews:
19
25
  #
20
26
  # config.view_component.preview_paths << "#{Rails.root}/lib/component_previews"
21
27
  #
22
28
  mattr_accessor :preview_paths, instance_writer: false
23
29
 
24
- # TODO: deprecated, remove in v3.0.0
30
+ # @deprecated Use `preview_paths` instead. Will be removed in v3.0.0.
25
31
  mattr_accessor :preview_path, instance_writer: false
26
32
 
27
- # Enable or disable component previews through app configuration:
28
- #
29
- # config.view_component.show_previews = true
30
- #
31
- # Defaults to +true+ for development environment
32
- #
33
- mattr_accessor :show_previews, instance_writer: false
34
-
35
- # Set the entry route for component previews through app configuration:
33
+ # Set the entry route for component previews:
36
34
  #
37
35
  # config.view_component.preview_route = "/previews"
38
36
  #
39
- # Defaults to +/rails/view_components+ when `show_previews' is enabled
37
+ # Defaults to `/rails/view_components` when `show_previews` is enabled.
40
38
  #
41
39
  mattr_accessor :preview_route, instance_writer: false do
42
40
  "/rails/view_components"
43
41
  end
44
42
 
45
- # Set the controller to be used for previewing components through app configuration:
43
+ # Set the controller used for previewing components:
46
44
  #
47
45
  # config.view_component.preview_controller = "MyPreviewController"
48
46
  #
49
- # Defaults to the provided +ViewComponentsController+
47
+ # Defaults to `ViewComponentsController`.
50
48
  #
51
49
  mattr_accessor :preview_controller, instance_writer: false do
52
50
  "ViewComponentsController"