view_component 2.50.0 → 2.69.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 (36) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +1 -1
  3. data/app/assets/vendor/prism.css +3 -195
  4. data/app/assets/vendor/prism.min.js +11 -11
  5. data/app/controllers/concerns/view_component/preview_actions.rb +97 -0
  6. data/app/controllers/view_components_controller.rb +1 -87
  7. data/app/helpers/preview_helper.rb +5 -5
  8. data/app/views/view_components/preview.html.erb +2 -2
  9. data/docs/CHANGELOG.md +427 -1
  10. data/lib/rails/generators/abstract_generator.rb +7 -9
  11. data/lib/rails/generators/component/component_generator.rb +5 -4
  12. data/lib/rails/generators/locale/component_generator.rb +1 -1
  13. data/lib/rails/generators/preview/component_generator.rb +1 -1
  14. data/lib/view_component/base.rb +152 -51
  15. data/lib/view_component/collection.rb +9 -2
  16. data/lib/view_component/compiler.rb +39 -18
  17. data/lib/view_component/config.rb +159 -0
  18. data/lib/view_component/content_areas.rb +1 -1
  19. data/lib/view_component/docs_builder_component.rb +1 -1
  20. data/lib/view_component/engine.rb +16 -30
  21. data/lib/view_component/polymorphic_slots.rb +28 -1
  22. data/lib/view_component/preview.rb +12 -9
  23. data/lib/view_component/render_component_helper.rb +1 -0
  24. data/lib/view_component/render_component_to_string_helper.rb +1 -1
  25. data/lib/view_component/render_to_string_monkey_patch.rb +1 -1
  26. data/lib/view_component/rendering_component_helper.rb +1 -1
  27. data/lib/view_component/rendering_monkey_patch.rb +1 -1
  28. data/lib/view_component/slot_v2.rb +4 -10
  29. data/lib/view_component/slotable.rb +5 -6
  30. data/lib/view_component/slotable_v2.rb +69 -21
  31. data/lib/view_component/test_helpers.rb +80 -8
  32. data/lib/view_component/translatable.rb +13 -14
  33. data/lib/view_component/version.rb +1 -1
  34. data/lib/view_component.rb +1 -0
  35. metadata +47 -18
  36. data/lib/view_component/previewable.rb +0 -62
@@ -0,0 +1,159 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "view_component/deprecation"
4
+
5
+ module ViewComponent
6
+ class Config
7
+ class << self
8
+ # `new` without any arguments initializes the default configuration, but
9
+ # it's important to differentiate in case that's no longer the case in
10
+ # future.
11
+ alias_method :default, :new
12
+
13
+ def defaults
14
+ ActiveSupport::OrderedOptions.new.merge!({
15
+ generate: ActiveSupport::OrderedOptions.new(false),
16
+ preview_controller: "ViewComponentsController",
17
+ preview_route: "/rails/view_components",
18
+ show_previews_source: false,
19
+ instrumentation_enabled: false,
20
+ render_monkey_patch_enabled: true,
21
+ view_component_path: "app/components",
22
+ component_parent_class: nil,
23
+ show_previews: Rails.env.development? || Rails.env.test?,
24
+ preview_paths: default_preview_paths,
25
+ test_controller: "ApplicationController",
26
+ default_preview_layout: nil
27
+ })
28
+ end
29
+
30
+ # @!attribute generate
31
+ # @return [ActiveSupport::OrderedOptions]
32
+ # The subset of configuration options relating to generators.
33
+ #
34
+ # All options under this namespace default to `false` unless otherwise
35
+ # stated.
36
+ #
37
+ # #### #sidecar
38
+ #
39
+ # Always generate a component with a sidecar directory:
40
+ #
41
+ # config.view_component.generate.sidecar = true
42
+ #
43
+ # #### #stimulus_controller
44
+ #
45
+ # Always generate a Stimulus controller alongside the component:
46
+ #
47
+ # config.view_component.generate.stimulus_controller = true
48
+ #
49
+ # #### #locale
50
+ #
51
+ # Always generate translations file alongside the component:
52
+ #
53
+ # config.view_component.generate.locale = true
54
+ #
55
+ # #### #distinct_locale_files
56
+ #
57
+ # Always generate as many translations files as available locales:
58
+ #
59
+ # config.view_component.generate.distinct_locale_files = true
60
+ #
61
+ # One file will be generated for each configured `I18n.available_locales`,
62
+ # falling back to `[:en]` when no `available_locales` is defined.
63
+ #
64
+ # #### #preview
65
+ #
66
+ # Always generate a preview alongside the component:
67
+ #
68
+ # config.view_component.generate.preview = true
69
+
70
+ # @!attribute preview_controller
71
+ # @return [String]
72
+ # The controller used for previewing components.
73
+ # Defaults to `ViewComponentsController`.
74
+
75
+ # @!attribute preview_route
76
+ # @return [String]
77
+ # The entry route for component previews.
78
+ # Defaults to `"/rails/view_components"`.
79
+
80
+ # @!attribute show_previews_source
81
+ # @return [Boolean]
82
+ # Whether to display source code previews in component previews.
83
+ # Defaults to `false`.
84
+
85
+ # @!attribute instrumentation_enabled
86
+ # @return [Boolean]
87
+ # Whether ActiveSupport notifications are enabled.
88
+ # Defaults to `false`.
89
+
90
+ # @!attribute render_monkey_patch_enabled
91
+ # @return [Boolean] Whether the #render method should be monkey patched.
92
+ # If this is disabled, use `#render_component` or
93
+ # `#render_component_to_string` instead.
94
+ # Defaults to `true`.
95
+
96
+ # @!attribute view_component_path
97
+ # @return [String]
98
+ # The path in which components, their templates, and their sidecars should
99
+ # be stored.
100
+ # Defaults to `"app/components"`.
101
+
102
+ # @!attribute component_parent_class
103
+ # @return [String]
104
+ # The parent class from which generated components will inherit.
105
+ # Defaults to `nil`. If this is falsy, generators will use
106
+ # `"ApplicationComponent"` if defined, `"ViewComponent::Base"` otherwise.
107
+
108
+ # @!attribute show_previews
109
+ # @return [Boolean]
110
+ # Whether component previews are enabled.
111
+ # Defaults to `true` in development and test environments.
112
+
113
+ # @!attribute preview_paths
114
+ # @return [Array<String>]
115
+ # The locations in which component previews will be looked up.
116
+ # Defaults to `['test/component/previews']` relative to your Rails root.
117
+
118
+ # @!attribute preview_path
119
+ # @deprecated Use #preview_paths instead. Will be removed in v3.0.0.
120
+
121
+ # @!attribute test_controller
122
+ # @return [String]
123
+ # The controller used for testing components.
124
+ # Can also be configured on a per-test basis using `#with_controller_class`.
125
+ # Defaults to `ApplicationController`.
126
+
127
+ # @!attribute default_preview_layout
128
+ # @return [String]
129
+ # A custom default layout used for the previews index page and individual
130
+ # previews.
131
+ # Defaults to `nil`. If this is falsy, `"component_preview"` is used.
132
+
133
+ def default_preview_paths
134
+ return [] unless defined?(Rails.root) && Dir.exist?("#{Rails.root}/test/components/previews")
135
+
136
+ ["#{Rails.root}/test/components/previews"]
137
+ end
138
+ end
139
+
140
+ def initialize
141
+ @config = self.class.defaults
142
+ end
143
+
144
+ def preview_path
145
+ preview_paths
146
+ end
147
+
148
+ def preview_path=(new_value)
149
+ ViewComponent::Deprecation.warn("`preview_path` will be removed in v3.0.0. Use `preview_paths` instead.")
150
+ self.preview_paths = Array.wrap(new_value)
151
+ end
152
+
153
+ delegate_missing_to :config
154
+
155
+ private
156
+
157
+ attr_reader :config
158
+ end
159
+ end
@@ -21,7 +21,7 @@ module ViewComponent
21
21
  )
22
22
  end
23
23
 
24
- if block_given?
24
+ if block
25
25
  content = view_context.capture(&block)
26
26
  end
27
27
 
@@ -28,7 +28,7 @@ module ViewComponent
28
28
  end
29
29
 
30
30
  def types
31
- " → [#{@method.tag(:return).types.join(',')}]" if @method.tag(:return)&.types && show_types?
31
+ " → [#{@method.tag(:return).types.join(",")}]" if @method.tag(:return)&.types && show_types?
32
32
  end
33
33
 
34
34
  def signature_or_name
@@ -1,11 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "rails"
4
+ require "view_component/base"
4
5
 
5
6
  module ViewComponent
6
7
  class Engine < Rails::Engine # :nodoc:
7
- config.view_component = ActiveSupport::OrderedOptions.new
8
- config.view_component.preview_paths ||= []
8
+ config.view_component = ViewComponent::Base.config
9
9
 
10
10
  rake_tasks do
11
11
  load "view_component/rails/tasks/view_component.rake"
@@ -14,25 +14,20 @@ module ViewComponent
14
14
  initializer "view_component.set_configs" do |app|
15
15
  options = app.config.view_component
16
16
 
17
+ %i[generate preview_controller preview_route show_previews_source].each do |config_option|
18
+ options[config_option] ||= ViewComponent::Base.public_send(config_option)
19
+ end
20
+ options.instrumentation_enabled = false if options.instrumentation_enabled.nil?
17
21
  options.render_monkey_patch_enabled = true if options.render_monkey_patch_enabled.nil?
18
22
  options.show_previews = Rails.env.development? || Rails.env.test? if options.show_previews.nil?
19
- options.show_previews_source ||= ViewComponent::Base.show_previews_source
20
23
  options.instrumentation_enabled = false if options.instrumentation_enabled.nil?
21
- options.preview_route ||= ViewComponent::Base.preview_route
22
- options.preview_controller ||= ViewComponent::Base.preview_controller
23
24
 
24
25
  if options.show_previews
26
+ # This is still necessary because when `config.view_component` is declared, `Rails.root` is unspecified.
25
27
  options.preview_paths << "#{Rails.root}/test/components/previews" if defined?(Rails.root) && Dir.exist?(
26
28
  "#{Rails.root}/test/components/previews"
27
29
  )
28
30
 
29
- if options.preview_path.present?
30
- ViewComponent::Deprecation.warn(
31
- "`preview_path` will be removed in v3.0.0. Use `preview_paths` instead."
32
- )
33
- options.preview_paths << options.preview_path
34
- end
35
-
36
31
  if options.show_previews_source
37
32
  require "method_source"
38
33
 
@@ -41,10 +36,6 @@ module ViewComponent
41
36
  end
42
37
  end
43
38
  end
44
-
45
- ActiveSupport.on_load(:view_component) do
46
- options.each { |k, v| send("#{k}=", v) if respond_to?("#{k}=") }
47
- end
48
39
  end
49
40
 
50
41
  initializer "view_component.enable_instrumentation" do |app|
@@ -61,7 +52,8 @@ module ViewComponent
61
52
  options = app.config.view_component
62
53
 
63
54
  if options.show_previews && !options.preview_paths.empty?
64
- ActiveSupport::Dependencies.autoload_paths.concat(options.preview_paths)
55
+ paths_to_add = options.preview_paths - ActiveSupport::Dependencies.autoload_paths
56
+ ActiveSupport::Dependencies.autoload_paths.concat(paths_to_add) if paths_to_add.any?
65
57
  end
66
58
  end
67
59
 
@@ -71,12 +63,6 @@ module ViewComponent
71
63
  end
72
64
  end
73
65
 
74
- initializer "view_component.compile_config_methods" do
75
- ActiveSupport.on_load(:view_component) do
76
- config.compile_methods! if config.respond_to?(:compile_methods!)
77
- end
78
- end
79
-
80
66
  initializer "view_component.monkey_patch_render" do |app|
81
67
  next if Rails.version.to_f >= 6.1 || !app.config.view_component.render_monkey_patch_enabled
82
68
 
@@ -93,7 +79,7 @@ module ViewComponent
93
79
  end
94
80
  end
95
81
 
96
- initializer "view_component.include_render_component" do |app|
82
+ initializer "view_component.include_render_component" do |_app|
97
83
  next if Rails.version.to_f >= 6.1
98
84
 
99
85
  ActiveSupport.on_load(:action_view) do
@@ -115,12 +101,12 @@ module ViewComponent
115
101
  end
116
102
  end
117
103
 
118
- initializer "compiler mode" do |app|
104
+ initializer "compiler mode" do |_app|
119
105
  ViewComponent::Compiler.mode = if Rails.env.development? || Rails.env.test?
120
- ViewComponent::Compiler::DEVELOPMENT_MODE
121
- else
122
- ViewComponent::Compiler::PRODUCTION_MODE
123
- end
106
+ ViewComponent::Compiler::DEVELOPMENT_MODE
107
+ else
108
+ ViewComponent::Compiler::PRODUCTION_MODE
109
+ end
124
110
  end
125
111
 
126
112
  config.after_initialize do |app|
@@ -159,7 +145,7 @@ unless defined?(ViewComponent::Base)
159
145
 
160
146
  ViewComponent::Deprecation.warn(
161
147
  "This manually engine loading is deprecated and will be removed in v3.0.0. " \
162
- "Remove `require \"view_component/engine\"`."
148
+ 'Remove `require "view_component/engine"`.'
163
149
  )
164
150
 
165
151
  require "view_component"
@@ -5,6 +5,17 @@ module ViewComponent
5
5
  # In older rails versions, using a concern isn't a good idea here because they appear to not work with
6
6
  # Module#prepend and class methods.
7
7
  def self.included(base)
8
+ if base != ViewComponent::Base
9
+ # :nocov:
10
+ location = Kernel.caller_locations(1, 1)[0]
11
+
12
+ warn(
13
+ "warning: ViewComponent::PolymorphicSlots is now included in ViewComponent::Base by default " \
14
+ "and can be removed from #{location.path}:#{location.lineno}"
15
+ )
16
+ # :nocov:
17
+ end
18
+
8
19
  base.singleton_class.prepend(ClassMethods)
9
20
  base.include(InstanceMethods)
10
21
  end
@@ -31,6 +42,10 @@ module ViewComponent
31
42
  define_method(getter_name) do
32
43
  get_slot(slot_name)
33
44
  end
45
+
46
+ define_method("#{getter_name}?") do
47
+ get_slot(slot_name).present?
48
+ end
34
49
  end
35
50
 
36
51
  renderable_hash = types.each_with_object({}) do |(poly_type, poly_callable), memo|
@@ -46,12 +61,24 @@ module ViewComponent
46
61
  end
47
62
 
48
63
  define_method(setter_name) do |*args, &block|
64
+ if _warn_on_deprecated_slot_setter
65
+ ViewComponent::Deprecation.warn(
66
+ "polymorphic slot setters like `#{setter_name}` are deprecated and will be removed in " \
67
+ "ViewComponent v3.0.0.\n\nUse `with_#{setter_name}` instead."
68
+ )
69
+ end
70
+
49
71
  set_polymorphic_slot(slot_name, poly_type, *args, &block)
50
72
  end
51
73
  ruby2_keywords(setter_name.to_sym) if respond_to?(:ruby2_keywords, true)
74
+
75
+ define_method("with_#{setter_name}") do |*args, &block|
76
+ set_polymorphic_slot(slot_name, poly_type, *args, &block)
77
+ end
78
+ ruby2_keywords(:"with_#{setter_name}") if respond_to?(:ruby2_keywords, true)
52
79
  end
53
80
 
54
- self.registered_slots[slot_name] = {
81
+ registered_slots[slot_name] = {
55
82
  collection: collection,
56
83
  renderable_hash: renderable_hash
57
84
  }
@@ -14,7 +14,7 @@ module ViewComponent # :nodoc:
14
14
  block: block,
15
15
  component: component,
16
16
  locals: {},
17
- template: "view_components/preview",
17
+ template: "view_components/preview"
18
18
  }
19
19
  end
20
20
 
@@ -30,7 +30,8 @@ module ViewComponent # :nodoc:
30
30
  class << self
31
31
  # Returns all component preview classes.
32
32
  def all
33
- load_previews if descendants.empty?
33
+ load_previews
34
+
34
35
  descendants
35
36
  end
36
37
 
@@ -65,10 +66,12 @@ module ViewComponent # :nodoc:
65
66
  name.chomp("Preview").underscore
66
67
  end
67
68
 
69
+ # rubocop:disable Style/TrivialAccessors
68
70
  # Setter for layout name.
69
71
  def layout(layout_name)
70
72
  @layout = layout_name
71
73
  end
74
+ # rubocop:enable Style/TrivialAccessors
72
75
 
73
76
  # Returns the relative path (from preview_path) to the preview example template if the template exists
74
77
  def preview_example_template_path(example)
@@ -86,26 +89,26 @@ module ViewComponent # :nodoc:
86
89
  end
87
90
 
88
91
  path = Dir["#{preview_path}/#{preview_name}_preview/#{example}.html.*"].first
89
- Pathname.new(path).
90
- relative_path_from(Pathname.new(preview_path)).
91
- to_s.
92
- sub(/\..*$/, "")
92
+ Pathname.new(path)
93
+ .relative_path_from(Pathname.new(preview_path))
94
+ .to_s
95
+ .sub(/\..*$/, "")
93
96
  end
94
97
 
95
98
  # Returns the method body for the example from the preview file.
96
99
  def preview_source(example)
97
- source = self.instance_method(example.to_sym).source.split("\n")
100
+ source = instance_method(example.to_sym).source.split("\n")
98
101
  source[1...(source.size - 1)].join("\n")
99
102
  end
100
103
 
101
- private
102
-
103
104
  def load_previews
104
105
  Array(preview_paths).each do |preview_path|
105
106
  Dir["#{preview_path}/**/*_preview.rb"].sort.each { |file| require_dependency file }
106
107
  end
107
108
  end
108
109
 
110
+ private
111
+
109
112
  def preview_paths
110
113
  Base.preview_paths
111
114
  end
@@ -3,6 +3,7 @@
3
3
  module ViewComponent
4
4
  module RenderComponentHelper # :nodoc:
5
5
  def render_component(component, &block)
6
+ component.set_original_view_context(__vc_original_view_context) if is_a?(ViewComponent::Base)
6
7
  component.render_in(self, &block)
7
8
  end
8
9
  end
@@ -3,7 +3,7 @@
3
3
  module ViewComponent
4
4
  module RenderComponentToStringHelper # :nodoc:
5
5
  def render_component_to_string(component)
6
- component.render_in(self.view_context)
6
+ component.render_in(view_context)
7
7
  end
8
8
  end
9
9
  end
@@ -4,7 +4,7 @@ module ViewComponent
4
4
  module RenderToStringMonkeyPatch # :nodoc:
5
5
  def render_to_string(options = {}, args = {})
6
6
  if options.respond_to?(:render_in)
7
- options.render_in(self.view_context)
7
+ options.render_in(view_context)
8
8
  else
9
9
  super
10
10
  end
@@ -3,7 +3,7 @@
3
3
  module ViewComponent
4
4
  module RenderingComponentHelper # :nodoc:
5
5
  def render_component(component)
6
- self.response_body = component.render_in(self.view_context)
6
+ self.response_body = component.render_in(view_context)
7
7
  end
8
8
  end
9
9
  end
@@ -4,7 +4,7 @@ module ViewComponent
4
4
  module RenderingMonkeyPatch # :nodoc:
5
5
  def render(options = {}, args = {})
6
6
  if options.respond_to?(:render_in)
7
- self.response_body = options.render_in(self.view_context)
7
+ self.response_body = options.render_in(view_context)
8
8
  else
9
9
  super
10
10
  end
@@ -45,18 +45,12 @@ module ViewComponent
45
45
  if defined?(@__vc_content_set_by_with_content)
46
46
  @__vc_component_instance.with_content(@__vc_content_set_by_with_content)
47
47
 
48
- view_context.capture do
49
- @__vc_component_instance.render_in(view_context)
50
- end
48
+ @__vc_component_instance.render_in(view_context)
51
49
  elsif defined?(@__vc_content_block)
52
- view_context.capture do
53
- # render_in is faster than `parent.render`
54
- @__vc_component_instance.render_in(view_context, &@__vc_content_block)
55
- end
50
+ # render_in is faster than `parent.render`
51
+ @__vc_component_instance.render_in(view_context, &@__vc_content_block)
56
52
  else
57
- view_context.capture do
58
- @__vc_component_instance.render_in(view_context)
59
- end
53
+ @__vc_component_instance.render_in(view_context)
60
54
  end
61
55
  elsif defined?(@__vc_content)
62
56
  @__vc_content
@@ -30,7 +30,7 @@ module ViewComponent
30
30
 
31
31
  slot_names.each do |slot_name|
32
32
  # Ensure slot_name isn't already declared
33
- if self.slots.key?(slot_name)
33
+ if slots.key?(slot_name)
34
34
  raise ArgumentError.new("#{slot_name} slot declared multiple times")
35
35
  end
36
36
 
@@ -73,7 +73,7 @@ module ViewComponent
73
73
  class_name = "ViewComponent::Slot" unless class_name.present?
74
74
 
75
75
  # Register the slot on the component
76
- self.slots[slot_name] = {
76
+ slots[slot_name] = {
77
77
  class_name: class_name,
78
78
  instance_variable_name: instance_variable_name,
79
79
  collection: collection
@@ -84,7 +84,7 @@ module ViewComponent
84
84
  def inherited(child)
85
85
  # Clone slot configuration into child class
86
86
  # see #test_slots_pollution
87
- child.slots = self.slots.clone
87
+ child.slots = slots.clone
88
88
 
89
89
  super
90
90
  end
@@ -106,7 +106,7 @@ module ViewComponent
106
106
  #
107
107
  def slot(slot_name, **args, &block)
108
108
  # Raise ArgumentError if `slot` doesn't exist
109
- unless slots.keys.include?(slot_name)
109
+ unless slots.key?(slot_name)
110
110
  raise ArgumentError.new "Unknown slot '#{slot_name}' - expected one of '#{slots.keys}'"
111
111
  end
112
112
 
@@ -123,8 +123,7 @@ module ViewComponent
123
123
  slot_instance = args.present? ? slot_class.new(**args) : slot_class.new
124
124
 
125
125
  # Capture block and assign to slot_instance#content
126
- # rubocop:disable Rails/OutputSafety
127
- slot_instance.content = view_context.capture(&block).to_s.strip.html_safe if block_given?
126
+ slot_instance.content = view_context.capture(&block).to_s.strip.html_safe if block
128
127
 
129
128
  if slot[:collection]
130
129
  # Initialize instance variable as an empty array