view_component 2.31.0 → 2.35.0

Sign up to get free protection for your applications and to get access to all the features.

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