view_component 2.33.0 → 2.37.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/controllers/view_components_controller.rb +1 -1
- 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} +151 -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 +144 -85
- data/lib/view_component/collection.rb +6 -2
- data/lib/view_component/compile_cache.rb +1 -0
- data/lib/view_component/compiler.rb +87 -53
- data/lib/view_component/content_areas.rb +57 -0
- data/lib/view_component/engine.rb +31 -3
- data/lib/view_component/instrumentation.rb +21 -0
- data/lib/view_component/preview.rb +19 -8
- data/lib/view_component/previewable.rb +16 -18
- data/lib/view_component/slot_v2.rb +34 -27
- data/lib/view_component/slotable.rb +2 -1
- data/lib/view_component/slotable_v2.rb +58 -24
- data/lib/view_component/test_helpers.rb +7 -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 +5 -2
- data/lib/yard/mattr_accessor_handler.rb +19 -0
- metadata +76 -39
@@ -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
|
@@ -31,7 +32,10 @@ module ViewComponent
|
|
31
32
|
if object.respond_to?(:to_ary)
|
32
33
|
object.to_ary
|
33
34
|
else
|
34
|
-
raise ArgumentError.new(
|
35
|
+
raise ArgumentError.new(
|
36
|
+
"The value of the first argument passed to `with_collection` isn't a valid collection. " \
|
37
|
+
"Make sure it responds to `to_ary`."
|
38
|
+
)
|
35
39
|
end
|
36
40
|
end
|
37
41
|
|
@@ -16,17 +16,22 @@ module ViewComponent
|
|
16
16
|
subclass_instance_methods = component_class.instance_methods(false)
|
17
17
|
|
18
18
|
if subclass_instance_methods.include?(:with_content) && raise_errors
|
19
|
-
raise ViewComponent::ComponentError.new(
|
19
|
+
raise ViewComponent::ComponentError.new(
|
20
|
+
"#{component_class} implements a reserved method, `#with_content`.\n\n" \
|
21
|
+
"To fix this issue, change the name of the method."
|
22
|
+
)
|
20
23
|
end
|
21
24
|
|
22
25
|
if template_errors.present?
|
23
26
|
raise ViewComponent::TemplateError.new(template_errors) if raise_errors
|
27
|
+
|
24
28
|
return false
|
25
29
|
end
|
26
30
|
|
27
31
|
if subclass_instance_methods.include?(:before_render_check)
|
28
32
|
ActiveSupport::Deprecation.warn(
|
29
|
-
"
|
33
|
+
"`#before_render_check` will be removed in v3.0.0.\n\n" \
|
34
|
+
"To fix this issue, use `#before_render` instead."
|
30
35
|
)
|
31
36
|
end
|
32
37
|
|
@@ -39,7 +44,10 @@ module ViewComponent
|
|
39
44
|
# Remove existing compiled template methods,
|
40
45
|
# as Ruby warns when redefining a method.
|
41
46
|
method_name = call_method_name(template[:variant])
|
42
|
-
|
47
|
+
|
48
|
+
if component_class.instance_methods.include?(method_name.to_sym)
|
49
|
+
component_class.send(:undef_method, method_name.to_sym)
|
50
|
+
end
|
43
51
|
|
44
52
|
component_class.class_eval <<-RUBY, template[:path], -1
|
45
53
|
def #{method_name}
|
@@ -61,7 +69,9 @@ module ViewComponent
|
|
61
69
|
attr_reader :component_class
|
62
70
|
|
63
71
|
def define_render_template_for
|
64
|
-
|
72
|
+
if component_class.instance_methods.include?(:render_template_for)
|
73
|
+
component_class.send(:undef_method, :render_template_for)
|
74
|
+
end
|
65
75
|
|
66
76
|
variant_elsifs = variants.compact.uniq.map do |variant|
|
67
77
|
"elsif variant.to_sym == :#{variant}\n #{call_method_name(variant)}"
|
@@ -77,72 +87,90 @@ module ViewComponent
|
|
77
87
|
end
|
78
88
|
end
|
79
89
|
RUBY
|
80
|
-
|
81
90
|
end
|
82
91
|
|
83
92
|
def template_errors
|
84
|
-
@
|
85
|
-
|
93
|
+
@__vc_template_errors ||=
|
94
|
+
begin
|
95
|
+
errors = []
|
86
96
|
|
87
|
-
|
88
|
-
|
89
|
-
|
97
|
+
if (templates + inline_calls).empty?
|
98
|
+
errors << "Could not find a template file or inline render method for #{component_class}."
|
99
|
+
end
|
90
100
|
|
91
|
-
|
92
|
-
|
93
|
-
|
101
|
+
if templates.count { |template| template[:variant].nil? } > 1
|
102
|
+
errors <<
|
103
|
+
"More than one template found for #{component_class}. " \
|
104
|
+
"There can only be one default template file per component."
|
105
|
+
end
|
94
106
|
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
107
|
+
invalid_variants =
|
108
|
+
templates.
|
109
|
+
group_by { |template| template[:variant] }.
|
110
|
+
map { |variant, grouped| variant if grouped.length > 1 }.
|
111
|
+
compact.
|
112
|
+
sort
|
113
|
+
|
114
|
+
unless invalid_variants.empty?
|
115
|
+
errors <<
|
116
|
+
"More than one template found for #{'variant'.pluralize(invalid_variants.count)} " \
|
117
|
+
"#{invalid_variants.map { |v| "'#{v}'" }.to_sentence} in #{component_class}. " \
|
118
|
+
"There can only be one template file per variant."
|
119
|
+
end
|
100
120
|
|
101
|
-
|
102
|
-
|
103
|
-
|
121
|
+
if templates.find { |template| template[:variant].nil? } && inline_calls_defined_on_self.include?(:call)
|
122
|
+
errors <<
|
123
|
+
"Template file and inline render method found for #{component_class}. " \
|
124
|
+
"There can only be a template file or inline render method per component."
|
125
|
+
end
|
104
126
|
|
105
|
-
|
106
|
-
|
107
|
-
end
|
127
|
+
duplicate_template_file_and_inline_variant_calls =
|
128
|
+
templates.pluck(:variant) & variants_from_inline_calls(inline_calls_defined_on_self)
|
108
129
|
|
109
|
-
|
110
|
-
|
130
|
+
unless duplicate_template_file_and_inline_variant_calls.empty?
|
131
|
+
count = duplicate_template_file_and_inline_variant_calls.count
|
111
132
|
|
112
|
-
|
113
|
-
|
133
|
+
errors <<
|
134
|
+
"Template #{'file'.pluralize(count)} and inline render #{'method'.pluralize(count)} " \
|
135
|
+
"found for #{'variant'.pluralize(count)} " \
|
136
|
+
"#{duplicate_template_file_and_inline_variant_calls.map { |v| "'#{v}'" }.to_sentence} " \
|
137
|
+
"in #{component_class}. " \
|
138
|
+
"There can only be a template file or inline render method per variant."
|
139
|
+
end
|
114
140
|
|
115
|
-
errors
|
141
|
+
errors
|
116
142
|
end
|
117
|
-
|
118
|
-
errors
|
119
|
-
end
|
120
143
|
end
|
121
144
|
|
122
145
|
def templates
|
123
|
-
@templates ||=
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
146
|
+
@templates ||=
|
147
|
+
begin
|
148
|
+
extensions = ActionView::Template.template_handler_extensions
|
149
|
+
|
150
|
+
component_class._sidecar_files(extensions).each_with_object([]) do |path, memo|
|
151
|
+
pieces = File.basename(path).split(".")
|
152
|
+
memo << {
|
153
|
+
path: path,
|
154
|
+
variant: pieces.second.split("+").second&.to_sym,
|
155
|
+
handler: pieces.last
|
156
|
+
}
|
157
|
+
end
|
133
158
|
end
|
134
|
-
end
|
135
159
|
end
|
136
160
|
|
137
161
|
def inline_calls
|
138
|
-
@inline_calls ||=
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
162
|
+
@inline_calls ||=
|
163
|
+
begin
|
164
|
+
# Fetch only ViewComponent ancestor classes to limit the scope of
|
165
|
+
# finding inline calls
|
166
|
+
view_component_ancestors =
|
167
|
+
(
|
168
|
+
component_class.ancestors.take_while { |ancestor| ancestor != ViewComponent::Base } -
|
169
|
+
component_class.included_modules
|
170
|
+
)
|
171
|
+
|
172
|
+
view_component_ancestors.flat_map { |ancestor| ancestor.instance_methods(false).grep(/^call/) }.uniq
|
173
|
+
end
|
146
174
|
end
|
147
175
|
|
148
176
|
def inline_calls_defined_on_self
|
@@ -150,7 +178,7 @@ module ViewComponent
|
|
150
178
|
end
|
151
179
|
|
152
180
|
def variants
|
153
|
-
@
|
181
|
+
@__vc_variants = (
|
154
182
|
templates.map { |template| template[:variant] } + variants_from_inline_calls(inline_calls)
|
155
183
|
).compact.uniq
|
156
184
|
end
|
@@ -168,7 +196,13 @@ module ViewComponent
|
|
168
196
|
if handler.method(:call).parameters.length > 1
|
169
197
|
handler.call(component_class, template)
|
170
198
|
else
|
171
|
-
handler.call(
|
199
|
+
handler.call(
|
200
|
+
OpenStruct.new(
|
201
|
+
source: template,
|
202
|
+
identifier: component_class.identifier,
|
203
|
+
type: component_class.type
|
204
|
+
)
|
205
|
+
)
|
172
206
|
end
|
173
207
|
end
|
174
208
|
|
@@ -0,0 +1,57 @@
|
|
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(
|
18
|
+
"Unknown content_area '#{area}' for #{self} - expected one of '#{content_areas}'.\n\n" \
|
19
|
+
"To fix this issue, add `with_content_area :#{area}` to #{self} or reference " \
|
20
|
+
"a valid content area."
|
21
|
+
)
|
22
|
+
end
|
23
|
+
|
24
|
+
if block_given?
|
25
|
+
content = view_context.capture(&block)
|
26
|
+
end
|
27
|
+
|
28
|
+
instance_variable_set("@#{area}".to_sym, content)
|
29
|
+
nil
|
30
|
+
end
|
31
|
+
|
32
|
+
class_methods do
|
33
|
+
def with_content_areas(*areas)
|
34
|
+
ActiveSupport::Deprecation.warn(
|
35
|
+
"`with_content_areas` is deprecated and will be removed in ViewComponent v3.0.0.\n\n" \
|
36
|
+
"Use slots (https://viewcomponent.org/guide/slots.html) instead."
|
37
|
+
)
|
38
|
+
|
39
|
+
if areas.include?(:content)
|
40
|
+
raise ArgumentError.new(
|
41
|
+
"#{self} defines a content area called :content, which is a reserved name. \n\n" \
|
42
|
+
"To fix this issue, use another name, such as `:body`."
|
43
|
+
)
|
44
|
+
end
|
45
|
+
|
46
|
+
areas.each do |area|
|
47
|
+
define_method area.to_sym do
|
48
|
+
content unless content_evaluated? # ensure content is loaded so content_areas will be defined
|
49
|
+
instance_variable_get(:"@#{area}") if instance_variable_defined?(:"@#{area}")
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
self.content_areas = areas
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -13,6 +13,7 @@ module ViewComponent
|
|
13
13
|
|
14
14
|
options.render_monkey_patch_enabled = true if options.render_monkey_patch_enabled.nil?
|
15
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
|
|
@@ -93,8 +110,19 @@ module ViewComponent
|
|
93
110
|
app.routes.prepend do
|
94
111
|
preview_controller = options.preview_controller.sub(/Controller$/, "").underscore
|
95
112
|
|
96
|
-
get
|
97
|
-
|
113
|
+
get(
|
114
|
+
options.preview_route,
|
115
|
+
to: "#{preview_controller}#index",
|
116
|
+
as: :preview_view_components,
|
117
|
+
internal: true
|
118
|
+
)
|
119
|
+
|
120
|
+
get(
|
121
|
+
"#{options.preview_route}/*path",
|
122
|
+
to: "#{preview_controller}#previews",
|
123
|
+
as: :preview_view_component,
|
124
|
+
internal: true
|
125
|
+
)
|
98
126
|
end
|
99
127
|
end
|
100
128
|
|
@@ -0,0 +1,21 @@
|
|
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(
|
13
|
+
"!render.view_component",
|
14
|
+
name: self.class.name,
|
15
|
+
identifier: self.class.identifier
|
16
|
+
) do
|
17
|
+
super(view_context, &block)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -71,19 +71,30 @@ 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
|
-
raise
|
80
|
+
raise(
|
81
|
+
PreviewTemplateError,
|
82
|
+
"A preview template for example #{example} does not exist.\n\n" \
|
83
|
+
"To fix this issue, create a template for the example."
|
84
|
+
)
|
80
85
|
end
|
81
86
|
|
82
87
|
path = Dir["#{preview_path}/#{preview_name}_preview/#{example}.html.*"].first
|
83
|
-
Pathname.new(path)
|
84
|
-
|
85
|
-
|
86
|
-
|
88
|
+
Pathname.new(path).
|
89
|
+
relative_path_from(Pathname.new(preview_path)).
|
90
|
+
to_s.
|
91
|
+
sub(/\..*$/, "")
|
92
|
+
end
|
93
|
+
|
94
|
+
# Returns the method body for the example from the preview file.
|
95
|
+
def preview_source(example)
|
96
|
+
source = self.instance_method(example.to_sym).source.split("\n")
|
97
|
+
source[1...(source.size - 1)].join("\n")
|
87
98
|
end
|
88
99
|
|
89
100
|
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"
|