view_component 2.49.1 → 3.23.2
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.
- checksums.yaml +4 -4
- data/LICENSE.txt +1 -1
- data/app/assets/vendor/prism.css +3 -195
- data/app/assets/vendor/prism.min.js +11 -11
- data/app/controllers/concerns/view_component/preview_actions.rb +108 -0
- data/app/controllers/view_components_controller.rb +1 -87
- data/app/controllers/view_components_system_test_controller.rb +30 -0
- data/app/helpers/preview_helper.rb +30 -12
- data/app/views/view_components/_preview_source.html.erb +3 -3
- data/app/views/view_components/preview.html.erb +2 -2
- data/docs/CHANGELOG.md +1653 -24
- data/lib/rails/generators/abstract_generator.rb +16 -10
- data/lib/rails/generators/component/component_generator.rb +8 -4
- data/lib/rails/generators/component/templates/component.rb.tt +3 -2
- data/lib/rails/generators/erb/component_generator.rb +1 -1
- data/lib/rails/generators/locale/component_generator.rb +4 -4
- data/lib/rails/generators/preview/component_generator.rb +17 -3
- data/lib/rails/generators/preview/templates/component_preview.rb.tt +5 -1
- data/lib/rails/generators/rspec/component_generator.rb +15 -3
- data/lib/rails/generators/rspec/templates/component_spec.rb.tt +3 -1
- data/lib/rails/generators/stimulus/component_generator.rb +8 -3
- data/lib/rails/generators/stimulus/templates/component_controller.ts.tt +9 -0
- data/lib/rails/generators/test_unit/templates/component_test.rb.tt +3 -1
- data/lib/view_component/base.rb +352 -196
- data/lib/view_component/capture_compatibility.rb +44 -0
- data/lib/view_component/collection.rb +28 -9
- data/lib/view_component/compiler.rb +162 -193
- data/lib/view_component/config.rb +225 -0
- data/lib/view_component/configurable.rb +17 -0
- data/lib/view_component/deprecation.rb +8 -0
- data/lib/view_component/engine.rb +74 -47
- data/lib/view_component/errors.rb +240 -0
- data/lib/view_component/inline_template.rb +55 -0
- data/lib/view_component/instrumentation.rb +10 -2
- data/lib/view_component/preview.rb +21 -19
- data/lib/view_component/rails/tasks/view_component.rake +11 -2
- data/lib/view_component/render_component_helper.rb +1 -0
- data/lib/view_component/render_component_to_string_helper.rb +1 -1
- data/lib/view_component/render_to_string_monkey_patch.rb +1 -1
- data/lib/view_component/rendering_component_helper.rb +1 -1
- data/lib/view_component/rendering_monkey_patch.rb +1 -1
- data/lib/view_component/slot.rb +119 -1
- data/lib/view_component/slotable.rb +393 -96
- data/lib/view_component/slotable_default.rb +20 -0
- data/lib/view_component/system_test_case.rb +13 -0
- data/lib/view_component/system_test_helpers.rb +27 -0
- data/lib/view_component/template.rb +134 -0
- data/lib/view_component/test_helpers.rb +208 -47
- data/lib/view_component/translatable.rb +51 -33
- data/lib/view_component/use_helpers.rb +42 -0
- data/lib/view_component/version.rb +5 -4
- data/lib/view_component/with_content_helper.rb +3 -8
- data/lib/view_component.rb +7 -12
- metadata +339 -57
- data/lib/rails/generators/component/USAGE +0 -13
- data/lib/view_component/content_areas.rb +0 -57
- data/lib/view_component/polymorphic_slots.rb +0 -73
- data/lib/view_component/preview_template_error.rb +0 -6
- data/lib/view_component/previewable.rb +0 -62
- data/lib/view_component/slot_v2.rb +0 -104
- data/lib/view_component/slotable_v2.rb +0 -307
- data/lib/view_component/template_error.rb +0 -9
- data/lib/yard/mattr_accessor_handler.rb +0 -19
@@ -0,0 +1,240 @@
|
|
1
|
+
module ViewComponent
|
2
|
+
class BaseError < StandardError
|
3
|
+
def initialize
|
4
|
+
super(self.class::MESSAGE)
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
class DuplicateSlotContentError < StandardError
|
9
|
+
MESSAGE =
|
10
|
+
"It looks like a block was provided after calling `with_content` on COMPONENT, " \
|
11
|
+
"which means that ViewComponent doesn't know which content to use.\n\n" \
|
12
|
+
"To fix this issue, use either `with_content` or a block."
|
13
|
+
|
14
|
+
def initialize(klass_name)
|
15
|
+
super(MESSAGE.gsub("COMPONENT", klass_name.to_s))
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class TemplateError < StandardError
|
20
|
+
def initialize(errors, templates = nil)
|
21
|
+
message = errors.join("\n")
|
22
|
+
|
23
|
+
super(message)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
class MultipleInlineTemplatesError < BaseError
|
28
|
+
MESSAGE = "Inline templates can only be defined once per-component."
|
29
|
+
end
|
30
|
+
|
31
|
+
class MissingPreviewTemplateError < StandardError
|
32
|
+
MESSAGE =
|
33
|
+
"A preview template for example EXAMPLE doesn't exist.\n\n" \
|
34
|
+
"To fix this issue, create a template for the example."
|
35
|
+
|
36
|
+
def initialize(example)
|
37
|
+
super(MESSAGE.gsub("EXAMPLE", example))
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
class DuplicateContentError < StandardError
|
42
|
+
MESSAGE =
|
43
|
+
"It looks like a block was provided after calling `with_content` on COMPONENT, " \
|
44
|
+
"which means that ViewComponent doesn't know which content to use.\n\n" \
|
45
|
+
"To fix this issue, use either `with_content` or a block."
|
46
|
+
|
47
|
+
def initialize(klass_name)
|
48
|
+
super(MESSAGE.gsub("COMPONENT", klass_name.to_s))
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
class EmptyOrInvalidInitializerError < StandardError
|
53
|
+
MESSAGE =
|
54
|
+
"The COMPONENT initializer is empty or invalid. " \
|
55
|
+
"It must accept the parameter `PARAMETER` to render it as a collection.\n\n" \
|
56
|
+
"To fix this issue, update the initializer to accept `PARAMETER`.\n\n" \
|
57
|
+
"See [the collections docs](https://viewcomponent.org/guide/collections.html) for more information on rendering collections."
|
58
|
+
|
59
|
+
def initialize(klass_name, parameter)
|
60
|
+
super(MESSAGE.gsub("COMPONENT", klass_name.to_s).gsub("PARAMETER", parameter.to_s))
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
class MissingCollectionArgumentError < StandardError
|
65
|
+
MESSAGE =
|
66
|
+
"The initializer for COMPONENT doesn't accept the parameter `PARAMETER`, " \
|
67
|
+
"which is required to render it as a collection.\n\n" \
|
68
|
+
"To fix this issue, update the initializer to accept `PARAMETER`.\n\n" \
|
69
|
+
"See [the collections docs](https://viewcomponent.org/guide/collections.html) for more information on rendering collections."
|
70
|
+
|
71
|
+
def initialize(klass_name, parameter)
|
72
|
+
super(MESSAGE.gsub("COMPONENT", klass_name.to_s).gsub("PARAMETER", parameter.to_s))
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
class ReservedParameterError < StandardError
|
77
|
+
MESSAGE =
|
78
|
+
"COMPONENT initializer can't accept the parameter `PARAMETER`, as it will override a " \
|
79
|
+
"public ViewComponent method. To fix this issue, rename the parameter."
|
80
|
+
|
81
|
+
def initialize(klass_name, parameter)
|
82
|
+
super(MESSAGE.gsub("COMPONENT", klass_name.to_s).gsub("PARAMETER", parameter.to_s))
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
class InvalidCollectionArgumentError < BaseError
|
87
|
+
MESSAGE =
|
88
|
+
"The value of the first argument passed to `with_collection` isn't a valid collection. " \
|
89
|
+
"Make sure it responds to `to_ary`."
|
90
|
+
end
|
91
|
+
|
92
|
+
class ContentSlotNameError < StandardError
|
93
|
+
MESSAGE =
|
94
|
+
"COMPONENT declares a slot named content, which is a reserved word in ViewComponent.\n\n" \
|
95
|
+
"Content passed to a ViewComponent as a block is captured and assigned to the `content` accessor without having to create an explicit slot.\n\n" \
|
96
|
+
"To fix this issue, either use the `content` accessor directly or choose a different slot name."
|
97
|
+
|
98
|
+
def initialize(klass_name)
|
99
|
+
super(MESSAGE.gsub("COMPONENT", klass_name.to_s))
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
class InvalidSlotDefinitionError < BaseError
|
104
|
+
MESSAGE =
|
105
|
+
"Invalid slot definition. Please pass a class, " \
|
106
|
+
"string, or callable (that is proc, lambda, etc)"
|
107
|
+
end
|
108
|
+
|
109
|
+
class InvalidSlotNameError < StandardError
|
110
|
+
end
|
111
|
+
|
112
|
+
class SlotPredicateNameError < InvalidSlotNameError
|
113
|
+
MESSAGE =
|
114
|
+
"COMPONENT declares a slot named SLOT_NAME, which ends with a question mark.\n\n" \
|
115
|
+
"This isn't allowed because the ViewComponent framework already provides predicate " \
|
116
|
+
"methods ending in `?`.\n\n" \
|
117
|
+
"To fix this issue, choose a different name."
|
118
|
+
|
119
|
+
def initialize(klass_name, slot_name)
|
120
|
+
super(MESSAGE.gsub("COMPONENT", klass_name.to_s).gsub("SLOT_NAME", slot_name.to_s))
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
class RedefinedSlotError < StandardError
|
125
|
+
MESSAGE =
|
126
|
+
"COMPONENT declares the SLOT_NAME slot multiple times.\n\n" \
|
127
|
+
"To fix this issue, choose a different slot name."
|
128
|
+
|
129
|
+
def initialize(klass_name, slot_name)
|
130
|
+
super(MESSAGE.gsub("COMPONENT", klass_name.to_s).gsub("SLOT_NAME", slot_name.to_s))
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
class ReservedSingularSlotNameError < InvalidSlotNameError
|
135
|
+
MESSAGE =
|
136
|
+
"COMPONENT declares a slot named SLOT_NAME, which is a reserved word in the ViewComponent framework.\n\n" \
|
137
|
+
"To fix this issue, choose a different name."
|
138
|
+
|
139
|
+
def initialize(klass_name, slot_name)
|
140
|
+
super(MESSAGE.gsub("COMPONENT", klass_name.to_s).gsub("SLOT_NAME", slot_name.to_s))
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
class ReservedPluralSlotNameError < InvalidSlotNameError
|
145
|
+
MESSAGE =
|
146
|
+
"COMPONENT declares a slot named SLOT_NAME, which is a reserved word in the ViewComponent framework.\n\n" \
|
147
|
+
"To fix this issue, choose a different name."
|
148
|
+
|
149
|
+
def initialize(klass_name, slot_name)
|
150
|
+
super(MESSAGE.gsub("COMPONENT", klass_name.to_s).gsub("SLOT_NAME", slot_name.to_s))
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
class UncountableSlotNameError < InvalidSlotNameError
|
155
|
+
MESSAGE =
|
156
|
+
"COMPONENT declares a slot named SLOT_NAME, which is an uncountable word\n\n" \
|
157
|
+
"To fix this issue, choose a different name."
|
158
|
+
|
159
|
+
def initialize(klass_name, slot_name)
|
160
|
+
super(MESSAGE.gsub("COMPONENT", klass_name.to_s).gsub("SLOT_NAME", slot_name.to_s))
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
class ContentAlreadySetForPolymorphicSlotError < StandardError
|
165
|
+
MESSAGE = "Content for slot SLOT_NAME has already been provided."
|
166
|
+
|
167
|
+
def initialize(slot_name)
|
168
|
+
super(MESSAGE.gsub("SLOT_NAME", slot_name.to_s))
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
class NilWithContentError < BaseError
|
173
|
+
MESSAGE =
|
174
|
+
"No content provided to `#with_content` for #{self}.\n\n" \
|
175
|
+
"To fix this issue, pass a value."
|
176
|
+
end
|
177
|
+
|
178
|
+
class TranslateCalledBeforeRenderError < BaseError
|
179
|
+
MESSAGE =
|
180
|
+
"`#translate` can't be used before rendering as it depends " \
|
181
|
+
"on the view context that only exists once a ViewComponent is passed to " \
|
182
|
+
"the Rails render pipeline.\n\n" \
|
183
|
+
"It's sometimes possible to fix this issue by moving code dependent on " \
|
184
|
+
"`#translate` to a [`#before_render` method](https://viewcomponent.org/api.html#before_render--void)."
|
185
|
+
end
|
186
|
+
|
187
|
+
class HelpersCalledBeforeRenderError < BaseError
|
188
|
+
MESSAGE =
|
189
|
+
"`#helpers` can't be used before rendering as it depends " \
|
190
|
+
"on the view context that only exists once a ViewComponent is passed to " \
|
191
|
+
"the Rails render pipeline.\n\n" \
|
192
|
+
"It's sometimes possible to fix this issue by moving code dependent on " \
|
193
|
+
"`#helpers` to a [`#before_render` method](https://viewcomponent.org/api.html#before_render--void)."
|
194
|
+
end
|
195
|
+
|
196
|
+
class ControllerCalledBeforeRenderError < BaseError
|
197
|
+
MESSAGE =
|
198
|
+
"`#controller` can't be used before rendering, as it depends " \
|
199
|
+
"on the view context that only exists once a ViewComponent is passed to " \
|
200
|
+
"the Rails render pipeline.\n\n" \
|
201
|
+
"It's sometimes possible to fix this issue by moving code dependent on " \
|
202
|
+
"`#controller` to a [`#before_render` method](https://viewcomponent.org/api.html#before_render--void)."
|
203
|
+
end
|
204
|
+
|
205
|
+
# :nocov:
|
206
|
+
class NoMatchingTemplatesForPreviewError < StandardError
|
207
|
+
MESSAGE = "Found 0 matches for templates for TEMPLATE_IDENTIFIER."
|
208
|
+
|
209
|
+
def initialize(template_identifier)
|
210
|
+
super(MESSAGE.gsub("TEMPLATE_IDENTIFIER", template_identifier))
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
class MultipleMatchingTemplatesForPreviewError < StandardError
|
215
|
+
MESSAGE = "Found multiple templates for TEMPLATE_IDENTIFIER."
|
216
|
+
|
217
|
+
def initialize(template_identifier)
|
218
|
+
super(MESSAGE.gsub("TEMPLATE_IDENTIFIER", template_identifier))
|
219
|
+
end
|
220
|
+
end
|
221
|
+
# :nocov:
|
222
|
+
|
223
|
+
class SystemTestControllerOnlyAllowedInTestError < BaseError
|
224
|
+
MESSAGE = "ViewComponent SystemTest controller must only be called in a test environment for security reasons."
|
225
|
+
end
|
226
|
+
|
227
|
+
class SystemTestControllerNefariousPathError < BaseError
|
228
|
+
MESSAGE = "ViewComponent SystemTest controller attempted to load a file outside of the expected directory."
|
229
|
+
end
|
230
|
+
|
231
|
+
class AlreadyDefinedPolymorphicSlotSetterError < StandardError
|
232
|
+
MESSAGE =
|
233
|
+
"A method called 'SETTER_METHOD_NAME' already exists and would be overwritten by the 'SETTER_NAME' polymorphic " \
|
234
|
+
"slot setter.\n\nPlease choose a different setter name."
|
235
|
+
|
236
|
+
def initialize(setter_method_name, setter_name)
|
237
|
+
super(MESSAGE.gsub("SETTER_METHOD_NAME", setter_method_name.to_s).gsub("SETTER_NAME", setter_name.to_s))
|
238
|
+
end
|
239
|
+
end
|
240
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ViewComponent # :nodoc:
|
4
|
+
module InlineTemplate
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
Template = Struct.new(:source, :language, :path, :lineno)
|
7
|
+
|
8
|
+
class_methods do
|
9
|
+
def method_missing(method, *args)
|
10
|
+
return super if !method.end_with?("_template")
|
11
|
+
|
12
|
+
if defined?(@__vc_inline_template_defined) && @__vc_inline_template_defined
|
13
|
+
raise MultipleInlineTemplatesError
|
14
|
+
end
|
15
|
+
|
16
|
+
if args.size != 1
|
17
|
+
raise ArgumentError, "wrong number of arguments (given #{args.size}, expected 1)"
|
18
|
+
end
|
19
|
+
|
20
|
+
ext = method.to_s.gsub("_template", "")
|
21
|
+
template = args.first
|
22
|
+
|
23
|
+
@__vc_inline_template_language = ext
|
24
|
+
|
25
|
+
caller = caller_locations(1..1)[0]
|
26
|
+
@__vc_inline_template = Template.new(
|
27
|
+
template,
|
28
|
+
ext,
|
29
|
+
caller.absolute_path || caller.path,
|
30
|
+
caller.lineno
|
31
|
+
)
|
32
|
+
|
33
|
+
@__vc_inline_template_defined = true
|
34
|
+
end
|
35
|
+
ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true)
|
36
|
+
|
37
|
+
def respond_to_missing?(method, include_all = false)
|
38
|
+
method.end_with?("_template") || super
|
39
|
+
end
|
40
|
+
|
41
|
+
def inline_template
|
42
|
+
@__vc_inline_template if defined?(@__vc_inline_template)
|
43
|
+
end
|
44
|
+
|
45
|
+
def inline_template_language
|
46
|
+
@__vc_inline_template_language if defined?(@__vc_inline_template_language)
|
47
|
+
end
|
48
|
+
|
49
|
+
def inherited(subclass)
|
50
|
+
super
|
51
|
+
subclass.instance_variable_set(:@__vc_inline_template_language, inline_template_language)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -10,14 +10,22 @@ module ViewComponent # :nodoc:
|
|
10
10
|
|
11
11
|
def render_in(view_context, &block)
|
12
12
|
ActiveSupport::Notifications.instrument(
|
13
|
-
|
13
|
+
notification_name,
|
14
14
|
{
|
15
15
|
name: self.class.name,
|
16
16
|
identifier: self.class.identifier
|
17
17
|
}
|
18
18
|
) do
|
19
|
-
super
|
19
|
+
super
|
20
20
|
end
|
21
21
|
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def notification_name
|
26
|
+
return "!render.view_component" if ViewComponent::Base.config.use_deprecated_instrumentation_name
|
27
|
+
|
28
|
+
"render.view_component"
|
29
|
+
end
|
22
30
|
end
|
23
31
|
end
|
@@ -4,6 +4,11 @@ require "active_support/descendants_tracker"
|
|
4
4
|
|
5
5
|
module ViewComponent # :nodoc:
|
6
6
|
class Preview
|
7
|
+
if defined?(Rails.application.routes.url_helpers)
|
8
|
+
# Workaround from https://stackoverflow.com/questions/20853526/make-yard-ignore-certain-class-extensions to appease YARD
|
9
|
+
send(:include, Rails.application.routes.url_helpers)
|
10
|
+
end
|
11
|
+
|
7
12
|
include ActionView::Helpers::TagHelper
|
8
13
|
include ActionView::Helpers::AssetTagHelper
|
9
14
|
extend ActiveSupport::DescendantsTracker
|
@@ -14,7 +19,7 @@ module ViewComponent # :nodoc:
|
|
14
19
|
block: block,
|
15
20
|
component: component,
|
16
21
|
locals: {},
|
17
|
-
template: "view_components/preview"
|
22
|
+
template: "view_components/preview"
|
18
23
|
}
|
19
24
|
end
|
20
25
|
|
@@ -30,7 +35,8 @@ module ViewComponent # :nodoc:
|
|
30
35
|
class << self
|
31
36
|
# Returns all component preview classes.
|
32
37
|
def all
|
33
|
-
load_previews
|
38
|
+
load_previews
|
39
|
+
|
34
40
|
descendants
|
35
41
|
end
|
36
42
|
|
@@ -65,47 +71,43 @@ module ViewComponent # :nodoc:
|
|
65
71
|
name.chomp("Preview").underscore
|
66
72
|
end
|
67
73
|
|
74
|
+
# rubocop:disable Style/TrivialAccessors
|
68
75
|
# Setter for layout name.
|
69
76
|
def layout(layout_name)
|
70
77
|
@layout = layout_name
|
71
78
|
end
|
79
|
+
# rubocop:enable Style/TrivialAccessors
|
72
80
|
|
73
81
|
# Returns the relative path (from preview_path) to the preview example template if the template exists
|
74
82
|
def preview_example_template_path(example)
|
75
83
|
preview_path =
|
76
|
-
Array(preview_paths).detect do |
|
77
|
-
Dir["#{
|
84
|
+
Array(preview_paths).detect do |path|
|
85
|
+
Dir["#{path}/#{preview_name}_preview/#{example}.html.*"].first
|
78
86
|
end
|
79
87
|
|
80
|
-
if preview_path.nil?
|
81
|
-
raise(
|
82
|
-
PreviewTemplateError,
|
83
|
-
"A preview template for example #{example} doesn't exist.\n\n" \
|
84
|
-
"To fix this issue, create a template for the example."
|
85
|
-
)
|
86
|
-
end
|
88
|
+
raise MissingPreviewTemplateError.new(example) if preview_path.nil?
|
87
89
|
|
88
90
|
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(/\..*$/, "")
|
91
|
+
Pathname.new(path)
|
92
|
+
.relative_path_from(Pathname.new(preview_path))
|
93
|
+
.to_s
|
94
|
+
.sub(/\..*$/, "")
|
93
95
|
end
|
94
96
|
|
95
97
|
# Returns the method body for the example from the preview file.
|
96
98
|
def preview_source(example)
|
97
|
-
source =
|
99
|
+
source = instance_method(example.to_sym).source.split("\n")
|
98
100
|
source[1...(source.size - 1)].join("\n")
|
99
101
|
end
|
100
102
|
|
101
|
-
private
|
102
|
-
|
103
103
|
def load_previews
|
104
104
|
Array(preview_paths).each do |preview_path|
|
105
|
-
Dir["#{preview_path}/**/*
|
105
|
+
Dir["#{preview_path}/**/*preview.rb"].sort.each { |file| require_dependency file }
|
106
106
|
end
|
107
107
|
end
|
108
108
|
|
109
|
+
private
|
110
|
+
|
109
111
|
def preview_paths
|
110
112
|
Base.preview_paths
|
111
113
|
end
|
@@ -3,9 +3,18 @@
|
|
3
3
|
task stats: "view_component:statsetup"
|
4
4
|
|
5
5
|
namespace :view_component do
|
6
|
-
task statsetup
|
6
|
+
task :statsetup do
|
7
|
+
# :nocov:
|
7
8
|
require "rails/code_statistics"
|
8
9
|
|
9
|
-
|
10
|
+
if Rails.root.join(ViewComponent::Base.view_component_path).directory?
|
11
|
+
::STATS_DIRECTORIES << ["ViewComponents", ViewComponent::Base.view_component_path]
|
12
|
+
end
|
13
|
+
|
14
|
+
if Rails.root.join("test/components").directory?
|
15
|
+
::STATS_DIRECTORIES << ["ViewComponent tests", "test/components"]
|
16
|
+
CodeStatistics::TEST_TYPES << "ViewComponent tests"
|
17
|
+
end
|
18
|
+
# :nocov:
|
10
19
|
end
|
11
20
|
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(
|
7
|
+
self.response_body = options.render_in(view_context)
|
8
8
|
else
|
9
9
|
super
|
10
10
|
end
|
data/lib/view_component/slot.rb
CHANGED
@@ -1,7 +1,125 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "view_component/with_content_helper"
|
4
|
+
|
3
5
|
module ViewComponent
|
4
6
|
class Slot
|
5
|
-
|
7
|
+
include ViewComponent::WithContentHelper
|
8
|
+
|
9
|
+
attr_writer :__vc_component_instance, :__vc_content_block, :__vc_content
|
10
|
+
|
11
|
+
def initialize(parent)
|
12
|
+
@parent = parent
|
13
|
+
end
|
14
|
+
|
15
|
+
def content?
|
16
|
+
return true if defined?(@__vc_content) && @__vc_content.present?
|
17
|
+
return true if defined?(@__vc_content_set_by_with_content) && @__vc_content_set_by_with_content.present?
|
18
|
+
return true if defined?(@__vc_content_block) && @__vc_content_block.present?
|
19
|
+
return false if !__vc_component_instance?
|
20
|
+
|
21
|
+
@__vc_component_instance.content?
|
22
|
+
end
|
23
|
+
|
24
|
+
def with_content(args)
|
25
|
+
if __vc_component_instance?
|
26
|
+
@__vc_component_instance.with_content(args)
|
27
|
+
else
|
28
|
+
super
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Used to render the slot content in the template
|
33
|
+
#
|
34
|
+
# There's currently 3 different values that may be set, that we can render.
|
35
|
+
#
|
36
|
+
# If the slot renderable is a component, the string class name of a
|
37
|
+
# component, or a function that returns a component, we render that
|
38
|
+
# component instance, returning the string.
|
39
|
+
#
|
40
|
+
# If the slot renderable is a function and returns a string, it's
|
41
|
+
# set as `@__vc_content` and is returned directly.
|
42
|
+
#
|
43
|
+
# If there is no slot renderable, we evaluate the block passed to
|
44
|
+
# the slot and return it.
|
45
|
+
def to_s
|
46
|
+
return @content if defined?(@content)
|
47
|
+
|
48
|
+
view_context = @parent.send(:view_context)
|
49
|
+
|
50
|
+
if defined?(@__vc_content_block) && defined?(@__vc_content_set_by_with_content)
|
51
|
+
raise DuplicateSlotContentError.new(self.class.name)
|
52
|
+
end
|
53
|
+
|
54
|
+
@content =
|
55
|
+
if __vc_component_instance?
|
56
|
+
@__vc_component_instance.__vc_original_view_context = @parent.__vc_original_view_context
|
57
|
+
|
58
|
+
if defined?(@__vc_content_block)
|
59
|
+
# render_in is faster than `parent.render`
|
60
|
+
@__vc_component_instance.render_in(view_context) do |*args|
|
61
|
+
return @__vc_content_block.call(*args) if @__vc_content_block&.source_location.nil?
|
62
|
+
|
63
|
+
block_context = @__vc_content_block.binding.receiver
|
64
|
+
|
65
|
+
if block_context.class < ActionView::Base
|
66
|
+
block_context.capture(*args, &@__vc_content_block)
|
67
|
+
else
|
68
|
+
@__vc_content_block.call(*args)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
else
|
72
|
+
@__vc_component_instance.render_in(view_context)
|
73
|
+
end
|
74
|
+
elsif defined?(@__vc_content)
|
75
|
+
@__vc_content
|
76
|
+
elsif defined?(@__vc_content_block)
|
77
|
+
view_context.capture(&@__vc_content_block)
|
78
|
+
elsif defined?(@__vc_content_set_by_with_content)
|
79
|
+
@__vc_content_set_by_with_content
|
80
|
+
end
|
81
|
+
|
82
|
+
@content = @content.to_s
|
83
|
+
end
|
84
|
+
|
85
|
+
# Allow access to public component methods via the wrapper
|
86
|
+
#
|
87
|
+
# for example
|
88
|
+
#
|
89
|
+
# calling `header.name` (where `header` is a slot) will call `name`
|
90
|
+
# on the `HeaderComponent` instance.
|
91
|
+
#
|
92
|
+
# Where the component may look like:
|
93
|
+
#
|
94
|
+
# class MyComponent < ViewComponent::Base
|
95
|
+
# has_one :header, HeaderComponent
|
96
|
+
#
|
97
|
+
# class HeaderComponent < ViewComponent::Base
|
98
|
+
# def name
|
99
|
+
# @name
|
100
|
+
# end
|
101
|
+
# end
|
102
|
+
# end
|
103
|
+
#
|
104
|
+
def method_missing(symbol, *args, &block)
|
105
|
+
@__vc_component_instance.public_send(symbol, *args, &block)
|
106
|
+
end
|
107
|
+
ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true)
|
108
|
+
|
109
|
+
def html_safe?
|
110
|
+
# :nocov:
|
111
|
+
to_s.html_safe?
|
112
|
+
# :nocov:
|
113
|
+
end
|
114
|
+
|
115
|
+
def respond_to_missing?(symbol, include_all = false)
|
116
|
+
__vc_component_instance? && @__vc_component_instance.respond_to?(symbol, include_all)
|
117
|
+
end
|
118
|
+
|
119
|
+
private
|
120
|
+
|
121
|
+
def __vc_component_instance?
|
122
|
+
defined?(@__vc_component_instance)
|
123
|
+
end
|
6
124
|
end
|
7
125
|
end
|