view_component 2.34.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/view_components/_preview_source.html.erb +17 -0
- data/app/views/view_components/preview.html.erb +6 -2
- data/{CHANGELOG.md → docs/CHANGELOG.md} +74 -2
- 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 +1 -0
- data/lib/view_component/base.rb +94 -72
- data/lib/view_component/collection.rb +2 -1
- data/lib/view_component/compile_cache.rb +1 -0
- data/lib/view_component/compiler.rb +53 -49
- data/lib/view_component/content_areas.rb +50 -0
- data/lib/view_component/engine.rb +6 -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 +19 -18
- 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 +74 -39
@@ -21,12 +21,13 @@ module ViewComponent
|
|
21
21
|
|
22
22
|
if template_errors.present?
|
23
23
|
raise ViewComponent::TemplateError.new(template_errors) if raise_errors
|
24
|
+
|
24
25
|
return false
|
25
26
|
end
|
26
27
|
|
27
28
|
if subclass_instance_methods.include?(:before_render_check)
|
28
29
|
ActiveSupport::Deprecation.warn(
|
29
|
-
"`before_render_check` will be removed in v3.0.0. Use
|
30
|
+
"`before_render_check` will be removed in v3.0.0. Use `#before_render` instead."
|
30
31
|
)
|
31
32
|
end
|
32
33
|
|
@@ -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
|
@@ -97,6 +97,12 @@ module ViewComponent
|
|
97
97
|
end
|
98
98
|
end
|
99
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
|
+
|
100
106
|
config.after_initialize do |app|
|
101
107
|
options = app.config.view_component
|
102
108
|
|
@@ -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
|
@@ -123,6 +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
|
126
127
|
slot_instance.content = view_context.capture(&block).to_s.strip.html_safe if block_given?
|
127
128
|
|
128
129
|
if slot[:collection]
|
@@ -135,7 +136,7 @@ module ViewComponent
|
|
135
136
|
# Append Slot instance to collection accessor Array
|
136
137
|
instance_variable_get(slot[:instance_variable_name]) << slot_instance
|
137
138
|
else
|
138
|
-
|
139
|
+
# Assign the Slot instance to the slot accessor
|
139
140
|
instance_variable_set(slot[:instance_variable_name], slot_instance)
|
140
141
|
end
|
141
142
|
|
@@ -190,10 +190,10 @@ module ViewComponent
|
|
190
190
|
content unless content_evaluated? # ensure content is loaded so slots will be defined
|
191
191
|
|
192
192
|
slot = self.class.registered_slots[slot_name]
|
193
|
-
@
|
193
|
+
@__vc_set_slots ||= {}
|
194
194
|
|
195
|
-
if @
|
196
|
-
return @
|
195
|
+
if @__vc_set_slots[slot_name]
|
196
|
+
return @__vc_set_slots[slot_name]
|
197
197
|
end
|
198
198
|
|
199
199
|
if slot[:collection]
|
@@ -217,42 +217,43 @@ module ViewComponent
|
|
217
217
|
# 2. Since we have to pass block content to components when calling
|
218
218
|
# `render`, evaluating the block here would require us to call
|
219
219
|
# `view_context.capture` twice, which is slower
|
220
|
-
slot.
|
220
|
+
slot.__vc_content_block = block if block_given?
|
221
221
|
|
222
222
|
# If class
|
223
223
|
if slot_definition[:renderable]
|
224
|
-
slot.
|
224
|
+
slot.__vc_component_instance = slot_definition[:renderable].new(*args, **kwargs)
|
225
225
|
# If class name as a string
|
226
226
|
elsif slot_definition[:renderable_class_name]
|
227
|
-
slot.
|
227
|
+
slot.__vc_component_instance = self.class.const_get(slot_definition[:renderable_class_name]).new(*args, **kwargs)
|
228
228
|
# If passed a lambda
|
229
229
|
elsif slot_definition[:renderable_function]
|
230
230
|
# Use `bind(self)` to ensure lambda is executed in the context of the
|
231
231
|
# current component. This is necessary to allow the lambda to access helper
|
232
232
|
# methods like `content_tag` as well as parent component state.
|
233
|
-
renderable_value =
|
234
|
-
|
235
|
-
|
233
|
+
renderable_value =
|
234
|
+
if block_given?
|
235
|
+
slot_definition[:renderable_function].bind(self).call(*args, **kwargs) do |*args, **kwargs|
|
236
|
+
view_context.capture(*args, **kwargs, &block)
|
237
|
+
end
|
238
|
+
else
|
239
|
+
slot_definition[:renderable_function].bind(self).call(*args, **kwargs)
|
236
240
|
end
|
237
|
-
else
|
238
|
-
slot_definition[:renderable_function].bind(self).call(*args, **kwargs)
|
239
|
-
end
|
240
241
|
|
241
242
|
# Function calls can return components, so if it's a component handle it specially
|
242
243
|
if renderable_value.respond_to?(:render_in)
|
243
|
-
slot.
|
244
|
+
slot.__vc_component_instance = renderable_value
|
244
245
|
else
|
245
|
-
slot.
|
246
|
+
slot.__vc_content = renderable_value
|
246
247
|
end
|
247
248
|
end
|
248
249
|
|
249
|
-
@
|
250
|
+
@__vc_set_slots ||= {}
|
250
251
|
|
251
252
|
if slot_definition[:collection]
|
252
|
-
@
|
253
|
-
@
|
253
|
+
@__vc_set_slots[slot_name] ||= []
|
254
|
+
@__vc_set_slots[slot_name].push(slot)
|
254
255
|
else
|
255
|
-
@
|
256
|
+
@__vc_set_slots[slot_name] = slot
|
256
257
|
end
|
257
258
|
|
258
259
|
slot
|