view_component 2.31.0 → 2.35.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 (33) 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/helpers/preview_helper.rb +19 -0
  6. data/app/views/test_mailer/test_email.html.erb +1 -0
  7. data/app/views/view_components/_preview_source.html.erb +17 -0
  8. data/app/views/view_components/preview.html.erb +6 -2
  9. data/{CHANGELOG.md → docs/CHANGELOG.md} +131 -1
  10. data/lib/rails/generators/abstract_generator.rb +29 -0
  11. data/lib/rails/generators/component/component_generator.rb +5 -5
  12. data/lib/rails/generators/erb/component_generator.rb +7 -16
  13. data/lib/rails/generators/haml/component_generator.rb +6 -16
  14. data/lib/rails/generators/slim/component_generator.rb +6 -16
  15. data/lib/view_component.rb +2 -0
  16. data/lib/view_component/base.rb +142 -74
  17. data/lib/view_component/collection.rb +3 -1
  18. data/lib/view_component/compile_cache.rb +1 -0
  19. data/lib/view_component/compiler.rb +58 -54
  20. data/lib/view_component/content_areas.rb +50 -0
  21. data/lib/view_component/engine.rb +19 -2
  22. data/lib/view_component/instrumentation.rb +17 -0
  23. data/lib/view_component/preview.rb +14 -7
  24. data/lib/view_component/previewable.rb +16 -18
  25. data/lib/view_component/slot_v2.rb +28 -27
  26. data/lib/view_component/slotable.rb +2 -1
  27. data/lib/view_component/slotable_v2.rb +23 -22
  28. data/lib/view_component/test_helpers.rb +6 -1
  29. data/lib/view_component/translatable.rb +6 -5
  30. data/lib/view_component/version.rb +1 -1
  31. data/lib/view_component/with_content_helper.rb +1 -1
  32. data/lib/yard/mattr_accessor_handler.rb +19 -0
  33. metadata +94 -43
@@ -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
@@ -38,6 +39,7 @@ module ViewComponent
38
39
  def component_options(item, iterator)
39
40
  item_options = { component.collection_parameter => item }
40
41
  item_options[component.collection_counter_parameter] = iterator.index + 1 if component.counter_argument_present?
42
+ item_options[component.collection_iteration_parameter] = iterator if component.iteration_argument_present?
41
43
 
42
44
  @options.merge(item_options)
43
45
  end
@@ -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)
@@ -15,21 +15,22 @@ module ViewComponent
15
15
 
16
16
  subclass_instance_methods = component_class.instance_methods(false)
17
17
 
18
- if subclass_instance_methods.include?(:before_render_check)
19
- ActiveSupport::Deprecation.warn(
20
- "`before_render_check` will be removed in v3.0.0. Use `before_render` instead."
21
- )
22
- end
23
-
24
18
  if subclass_instance_methods.include?(:with_content) && raise_errors
25
19
  raise ViewComponent::ComponentError.new("#{component_class} implements a reserved method, `with_content`.")
26
20
  end
27
21
 
28
22
  if template_errors.present?
29
23
  raise ViewComponent::TemplateError.new(template_errors) if raise_errors
24
+
30
25
  return false
31
26
  end
32
27
 
28
+ if subclass_instance_methods.include?(:before_render_check)
29
+ ActiveSupport::Deprecation.warn(
30
+ "`before_render_check` will be removed in v3.0.0. Use `#before_render` instead."
31
+ )
32
+ end
33
+
33
34
  if raise_errors
34
35
  component_class.validate_initialization_parameters!
35
36
  component_class.validate_collection_parameter!
@@ -77,72 +78,75 @@ module ViewComponent
77
78
  end
78
79
  end
79
80
  RUBY
80
-
81
81
  end
82
82
 
83
83
  def template_errors
84
- @_template_errors ||= begin
85
- errors = []
84
+ @__vc_template_errors ||=
85
+ begin
86
+ errors = []
86
87
 
87
- if (templates + inline_calls).empty?
88
- errors << "Could not find a template file or inline render method for #{component_class}."
89
- end
88
+ if (templates + inline_calls).empty?
89
+ errors << "Could not find a template file or inline render method for #{component_class}."
90
+ end
90
91
 
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
92
+ if templates.count { |template| template[:variant].nil? } > 1
93
+ errors << "More than one template found for #{component_class}. There can only be one default template file per component."
94
+ end
94
95
 
95
- invalid_variants = templates
96
- .group_by { |template| template[:variant] }
97
- .map { |variant, grouped| variant if grouped.length > 1 }
98
- .compact
99
- .sort
96
+ invalid_variants =
97
+ templates.
98
+ group_by { |template| template[:variant] }.
99
+ map { |variant, grouped| variant if grouped.length > 1 }.
100
+ compact.
101
+ sort
100
102
 
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
103
+ unless invalid_variants.empty?
104
+ 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."
105
+ end
104
106
 
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
107
+ if templates.find { |template| template[:variant].nil? } && inline_calls_defined_on_self.include?(:call)
108
+ errors << "Template file and inline render method found for #{component_class}. There can only be a template file or inline render method per component."
109
+ end
108
110
 
109
- duplicate_template_file_and_inline_variant_calls =
110
- templates.pluck(:variant) & variants_from_inline_calls(inline_calls_defined_on_self)
111
+ duplicate_template_file_and_inline_variant_calls =
112
+ templates.pluck(:variant) & variants_from_inline_calls(inline_calls_defined_on_self)
111
113
 
112
- unless duplicate_template_file_and_inline_variant_calls.empty?
113
- count = duplicate_template_file_and_inline_variant_calls.count
114
+ unless duplicate_template_file_and_inline_variant_calls.empty?
115
+ count = duplicate_template_file_and_inline_variant_calls.count
114
116
 
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."
116
- end
117
+ 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."
118
+ end
117
119
 
118
- errors
119
- end
120
+ errors
121
+ end
120
122
  end
121
123
 
122
124
  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
- }
125
+ @templates ||=
126
+ begin
127
+ extensions = ActionView::Template.template_handler_extensions
128
+
129
+ component_class._sidecar_files(extensions).each_with_object([]) do |path, memo|
130
+ pieces = File.basename(path).split(".")
131
+ memo << {
132
+ path: path,
133
+ variant: pieces.second.split("+").second&.to_sym,
134
+ handler: pieces.last
135
+ }
136
+ end
133
137
  end
134
- end
135
138
  end
136
139
 
137
140
  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
141
+ @inline_calls ||=
142
+ begin
143
+ # Fetch only ViewComponent ancestor classes to limit the scope of
144
+ # finding inline calls
145
+ view_component_ancestors =
146
+ component_class.ancestors.take_while { |ancestor| ancestor != ViewComponent::Base } - component_class.included_modules
147
+
148
+ view_component_ancestors.flat_map { |ancestor| ancestor.instance_methods(false).grep(/^call/) }.uniq
149
+ end
146
150
  end
147
151
 
148
152
  def inline_calls_defined_on_self
@@ -150,7 +154,7 @@ module ViewComponent
150
154
  end
151
155
 
152
156
  def variants
153
- @_variants = (
157
+ @__vc_variants = (
154
158
  templates.map { |template| template[:variant] } + variants_from_inline_calls(inline_calls)
155
159
  ).compact.uniq
156
160
  end
@@ -0,0 +1,50 @@
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 "Unknown content_area '#{area}' - expected one of '#{content_areas}'"
18
+ end
19
+
20
+ if block_given?
21
+ content = view_context.capture(&block)
22
+ end
23
+
24
+ instance_variable_set("@#{area}".to_sym, content)
25
+ nil
26
+ end
27
+
28
+ class_methods do
29
+ def with_content_areas(*areas)
30
+ ActiveSupport::Deprecation.warn(
31
+ "`with_content_areas` is deprecated and will be removed in ViewComponent v3.0.0.\n" \
32
+ "Use slots (https://viewcomponent.org/guide/slots.html) instead."
33
+ )
34
+
35
+ if areas.include?(:content)
36
+ raise ArgumentError.new ":content is a reserved content area name. Please use another name, such as ':body'"
37
+ end
38
+
39
+ areas.each do |area|
40
+ define_method area.to_sym do
41
+ content unless content_evaluated? # ensure content is loaded so content_areas will be defined
42
+ instance_variable_get(:"@#{area}") if instance_variable_defined?(:"@#{area}")
43
+ end
44
+ end
45
+
46
+ self.content_areas = areas
47
+ end
48
+ end
49
+ end
50
+ end
@@ -12,7 +12,8 @@ module ViewComponent
12
12
  options = app.config.view_component
13
13
 
14
14
  options.render_monkey_patch_enabled = true if options.render_monkey_patch_enabled.nil?
15
- options.show_previews = Rails.env.development? if options.show_previews.nil?
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
 
@@ -0,0 +1,17 @@
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("!render.view_component", name: self.class.name, identifier: self.class.identifier) do
13
+ super(view_context, &block)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -71,19 +71,26 @@ 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
80
  raise PreviewTemplateError, "preview template for example #{example} does not exist"
80
81
  end
81
82
 
82
83
  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(/\..*$/, "")
84
+ Pathname.new(path).
85
+ relative_path_from(Pathname.new(preview_path)).
86
+ to_s.
87
+ sub(/\..*$/, "")
88
+ end
89
+
90
+ # Returns the method body for the example from the preview file.
91
+ def preview_source(example)
92
+ source = self.instance_method(example.to_sym).source.split("\n")
93
+ source[1...(source.size - 1)].join("\n")
87
94
  end
88
95
 
89
96
  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"
@@ -6,7 +6,7 @@ module ViewComponent
6
6
  class SlotV2
7
7
  include ViewComponent::WithContentHelper
8
8
 
9
- attr_writer :_component_instance, :_content_block, :_content
9
+ attr_writer :__vc_component_instance, :__vc_content_block, :__vc_content
10
10
 
11
11
  def initialize(parent)
12
12
  @parent = parent
@@ -21,7 +21,7 @@ module ViewComponent
21
21
  # component instance, returning the string.
22
22
  #
23
23
  # If the slot renderable is a function and returns a string, it is
24
- # set as `@_content` and is returned directly.
24
+ # set as `@__vc_content` and is returned directly.
25
25
  #
26
26
  # If there is no slot renderable, we evaluate the block passed to
27
27
  # the slot and return it.
@@ -30,32 +30,33 @@ module ViewComponent
30
30
 
31
31
  view_context = @parent.send(:view_context)
32
32
 
33
- raise ArgumentError.new("Block provided after calling `with_content`. Use one or the other.") if defined?(@_content_block) && defined?(@_content_set_by_with_content)
33
+ raise ArgumentError.new("Block provided after calling `with_content`. Use one or the other.") if defined?(@__vc_content_block) && defined?(@__vc_content_set_by_with_content)
34
34
 
35
- @content = if defined?(@_component_instance)
36
- if defined?(@_content_set_by_with_content)
37
- @_component_instance.with_content(@_content_set_by_with_content)
35
+ @content =
36
+ if defined?(@__vc_component_instance)
37
+ if defined?(@__vc_content_set_by_with_content)
38
+ @__vc_component_instance.with_content(@__vc_content_set_by_with_content)
38
39
 
39
- view_context.capture do
40
- @_component_instance.render_in(view_context)
40
+ view_context.capture do
41
+ @__vc_component_instance.render_in(view_context)
42
+ end
43
+ elsif defined?(@__vc_content_block)
44
+ view_context.capture do
45
+ # render_in is faster than `parent.render`
46
+ @__vc_component_instance.render_in(view_context, &@__vc_content_block)
47
+ end
48
+ else
49
+ view_context.capture do
50
+ @__vc_component_instance.render_in(view_context)
51
+ end
41
52
  end
42
- elsif defined?(@_content_block)
43
- view_context.capture do
44
- # render_in is faster than `parent.render`
45
- @_component_instance.render_in(view_context, &@_content_block)
46
- end
47
- else
48
- view_context.capture do
49
- @_component_instance.render_in(view_context)
50
- end
51
- end
52
- elsif defined?(@_content)
53
- @_content
54
- elsif defined?(@_content_block)
55
- view_context.capture(&@_content_block)
56
- elsif defined?(@_content_set_by_with_content)
57
- @_content_set_by_with_content
58
- end
53
+ elsif defined?(@__vc_content)
54
+ @__vc_content
55
+ elsif defined?(@__vc_content_block)
56
+ view_context.capture(&@__vc_content_block)
57
+ elsif defined?(@__vc_content_set_by_with_content)
58
+ @__vc_content_set_by_with_content
59
+ end
59
60
 
60
61
  @content
61
62
  end
@@ -80,7 +81,7 @@ module ViewComponent
80
81
  # end
81
82
  #
82
83
  def method_missing(symbol, *args, &block)
83
- @_component_instance.public_send(symbol, *args, &block)
84
+ @__vc_component_instance.public_send(symbol, *args, &block)
84
85
  end
85
86
 
86
87
  def html_safe?
@@ -88,7 +89,7 @@ module ViewComponent
88
89
  end
89
90
 
90
91
  def respond_to_missing?(symbol, include_all = false)
91
- defined?(@_component_instance) && @_component_instance.respond_to?(symbol, include_all)
92
+ defined?(@__vc_component_instance) && @__vc_component_instance.respond_to?(symbol, include_all)
92
93
  end
93
94
  end
94
95
  end