view_component 2.50.0 → 2.69.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 (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