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.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/app/assets/vendor/prism.css +196 -0
- data/app/assets/vendor/prism.min.js +12 -0
- data/app/helpers/preview_helper.rb +19 -0
- data/app/views/test_mailer/test_email.html.erb +1 -0
- data/app/views/view_components/_preview_source.html.erb +17 -0
- data/app/views/view_components/preview.html.erb +6 -2
- data/{CHANGELOG.md → docs/CHANGELOG.md} +131 -1
- data/lib/rails/generators/abstract_generator.rb +29 -0
- data/lib/rails/generators/component/component_generator.rb +5 -5
- data/lib/rails/generators/erb/component_generator.rb +7 -16
- data/lib/rails/generators/haml/component_generator.rb +6 -16
- data/lib/rails/generators/slim/component_generator.rb +6 -16
- data/lib/view_component.rb +2 -0
- data/lib/view_component/base.rb +142 -74
- data/lib/view_component/collection.rb +3 -1
- data/lib/view_component/compile_cache.rb +1 -0
- data/lib/view_component/compiler.rb +58 -54
- data/lib/view_component/content_areas.rb +50 -0
- data/lib/view_component/engine.rb +19 -2
- data/lib/view_component/instrumentation.rb +17 -0
- data/lib/view_component/preview.rb +14 -7
- data/lib/view_component/previewable.rb +16 -18
- data/lib/view_component/slot_v2.rb +28 -27
- data/lib/view_component/slotable.rb +2 -1
- data/lib/view_component/slotable_v2.rb +23 -22
- data/lib/view_component/test_helpers.rb +6 -1
- data/lib/view_component/translatable.rb +6 -5
- data/lib/view_component/version.rb +1 -1
- data/lib/view_component/with_content_helper.rb +1 -1
- data/lib/yard/mattr_accessor_handler.rb +19 -0
- 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
|
@@ -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
|
-
@
|
85
|
-
|
84
|
+
@__vc_template_errors ||=
|
85
|
+
begin
|
86
|
+
errors = []
|
86
87
|
|
87
|
-
|
88
|
-
|
89
|
-
|
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
|
-
|
92
|
-
|
93
|
-
|
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
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
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
|
-
|
102
|
-
|
103
|
-
|
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
|
-
|
106
|
-
|
107
|
-
|
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
|
-
|
110
|
-
|
111
|
+
duplicate_template_file_and_inline_variant_calls =
|
112
|
+
templates.pluck(:variant) & variants_from_inline_calls(inline_calls_defined_on_self)
|
111
113
|
|
112
|
-
|
113
|
-
|
114
|
+
unless duplicate_template_file_and_inline_variant_calls.empty?
|
115
|
+
count = duplicate_template_file_and_inline_variant_calls.count
|
114
116
|
|
115
|
-
|
116
|
-
|
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
|
-
|
119
|
-
|
120
|
+
errors
|
121
|
+
end
|
120
122
|
end
|
121
123
|
|
122
124
|
def templates
|
123
|
-
@templates ||=
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
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 ||=
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
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
|
-
@
|
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 =
|
75
|
-
|
76
|
-
|
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
|
-
|
85
|
-
|
86
|
-
|
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
|
5
|
+
module ViewComponent
|
6
6
|
module Previewable
|
7
7
|
extend ActiveSupport::Concern
|
8
8
|
|
9
9
|
included do
|
10
|
-
#
|
10
|
+
# Enable or disable component previews:
|
11
11
|
#
|
12
|
-
# config.view_component.
|
12
|
+
# config.view_component.show_previews = true
|
13
13
|
#
|
14
|
-
#
|
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
|
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
|
-
#
|
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
|
-
#
|
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
|
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
|
43
|
+
# Set the controller used for previewing components:
|
46
44
|
#
|
47
45
|
# config.view_component.preview_controller = "MyPreviewController"
|
48
46
|
#
|
49
|
-
# Defaults to
|
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 :
|
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 `@
|
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?(@
|
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 =
|
36
|
-
if defined?(@
|
37
|
-
|
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
|
-
|
40
|
-
|
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?(@
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
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
|
-
@
|
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?(@
|
92
|
+
defined?(@__vc_component_instance) && @__vc_component_instance.respond_to?(symbol, include_all)
|
92
93
|
end
|
93
94
|
end
|
94
95
|
end
|