view_component 2.83.0 → 3.21.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.
- checksums.yaml +4 -4
- data/app/controllers/concerns/view_component/preview_actions.rb +5 -1
- data/app/controllers/view_components_system_test_controller.rb +24 -1
- data/app/helpers/preview_helper.rb +22 -4
- data/app/views/view_components/_preview_source.html.erb +2 -2
- data/docs/CHANGELOG.md +807 -1
- data/lib/rails/generators/abstract_generator.rb +9 -1
- data/lib/rails/generators/component/component_generator.rb +2 -1
- 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 +3 -3
- data/lib/rails/generators/preview/templates/component_preview.rb.tt +2 -0
- data/lib/rails/generators/rspec/component_generator.rb +15 -3
- data/lib/rails/generators/rspec/templates/component_spec.rb.tt +1 -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 +1 -1
- data/lib/view_component/base.rb +169 -164
- data/lib/view_component/capture_compatibility.rb +44 -0
- data/lib/view_component/collection.rb +20 -8
- data/lib/view_component/compiler.rb +166 -207
- data/lib/view_component/config.rb +63 -14
- data/lib/view_component/deprecation.rb +1 -1
- data/lib/view_component/docs_builder_component.html.erb +5 -1
- data/lib/view_component/docs_builder_component.rb +28 -9
- data/lib/view_component/engine.rb +58 -28
- 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 +7 -8
- data/lib/view_component/rails/tasks/view_component.rake +11 -2
- data/lib/view_component/slot.rb +119 -1
- data/lib/view_component/slotable.rb +394 -94
- data/lib/view_component/slotable_default.rb +20 -0
- data/lib/view_component/system_test_helpers.rb +5 -5
- data/lib/view_component/template.rb +134 -0
- data/lib/view_component/test_helpers.rb +138 -59
- data/lib/view_component/translatable.rb +45 -26
- data/lib/view_component/use_helpers.rb +42 -0
- data/lib/view_component/version.rb +4 -3
- data/lib/view_component/with_content_helper.rb +3 -8
- data/lib/view_component.rb +3 -12
- metadata +277 -38
- data/lib/view_component/content_areas.rb +0 -56
- data/lib/view_component/polymorphic_slots.rb +0 -103
- data/lib/view_component/preview_template_error.rb +0 -6
- data/lib/view_component/slot_v2.rb +0 -98
- data/lib/view_component/slotable_v2.rb +0 -391
- data/lib/view_component/template_error.rb +0 -9
@@ -2,21 +2,40 @@
|
|
2
2
|
|
3
3
|
module ViewComponent
|
4
4
|
class DocsBuilderComponent < Base
|
5
|
-
class Section < Struct.new(:heading, :methods, :show_types, keyword_init: true)
|
6
|
-
def initialize(heading: nil, methods: [], show_types: true)
|
5
|
+
class Section < Struct.new(:heading, :methods, :error_klasses, :show_types, keyword_init: true)
|
6
|
+
def initialize(heading: nil, methods: [], error_klasses: [], show_types: true)
|
7
7
|
methods.sort_by! { |method| method[:name] }
|
8
|
+
error_klasses.sort!
|
8
9
|
super
|
9
10
|
end
|
10
11
|
end
|
11
12
|
|
12
|
-
class
|
13
|
-
def initialize(
|
14
|
-
@
|
15
|
-
|
13
|
+
class ErrorKlassDoc < ViewComponent::Base
|
14
|
+
def initialize(error_klass, _show_types)
|
15
|
+
@error_klass = error_klass
|
16
|
+
end
|
17
|
+
|
18
|
+
def klass_name
|
19
|
+
@error_klass.gsub("ViewComponent::", "").gsub("::MESSAGE", "")
|
16
20
|
end
|
17
21
|
|
18
|
-
def
|
19
|
-
@
|
22
|
+
def error_message
|
23
|
+
ViewComponent.const_get(@error_klass)
|
24
|
+
end
|
25
|
+
|
26
|
+
def call
|
27
|
+
<<~DOCS.chomp
|
28
|
+
`#{klass_name}`
|
29
|
+
|
30
|
+
#{error_message}
|
31
|
+
DOCS
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
class MethodDoc < ViewComponent::Base
|
36
|
+
def initialize(method, show_types = true)
|
37
|
+
@method = method
|
38
|
+
@show_types = show_types
|
20
39
|
end
|
21
40
|
|
22
41
|
def deprecated?
|
@@ -28,7 +47,7 @@ module ViewComponent
|
|
28
47
|
end
|
29
48
|
|
30
49
|
def types
|
31
|
-
" → [#{@method.tag(:return).types.join(",")}]" if @method.tag(:return)&.types && show_types
|
50
|
+
" → [#{@method.tag(:return).types.join(",")}]" if @method.tag(:return)&.types && @show_types
|
32
51
|
end
|
33
52
|
|
34
53
|
def signature_or_name
|
@@ -1,14 +1,29 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "rails"
|
4
|
-
require "view_component/
|
4
|
+
require "view_component/config"
|
5
|
+
require "view_component/deprecation"
|
5
6
|
|
6
7
|
module ViewComponent
|
7
8
|
class Engine < Rails::Engine # :nodoc:
|
8
|
-
config.view_component = ViewComponent::
|
9
|
+
config.view_component = ViewComponent::Config.current
|
9
10
|
|
10
|
-
|
11
|
-
|
11
|
+
if Rails.version.to_f < 8.0
|
12
|
+
rake_tasks do
|
13
|
+
load "view_component/rails/tasks/view_component.rake"
|
14
|
+
end
|
15
|
+
else
|
16
|
+
initializer "view_component.stats_directories" do |app|
|
17
|
+
require "rails/code_statistics"
|
18
|
+
|
19
|
+
if Rails.root.join(ViewComponent::Base.view_component_path).directory?
|
20
|
+
Rails::CodeStatistics.register_directory("ViewComponents", ViewComponent::Base.view_component_path)
|
21
|
+
end
|
22
|
+
|
23
|
+
if Rails.root.join("test/components").directory?
|
24
|
+
Rails::CodeStatistics.register_directory("ViewComponent tests", "test/components", test_directory: true)
|
25
|
+
end
|
26
|
+
end
|
12
27
|
end
|
13
28
|
|
14
29
|
initializer "view_component.set_configs" do |app|
|
@@ -40,13 +55,27 @@ module ViewComponent
|
|
40
55
|
initializer "view_component.enable_instrumentation" do |app|
|
41
56
|
ActiveSupport.on_load(:view_component) do
|
42
57
|
if app.config.view_component.instrumentation_enabled.present?
|
43
|
-
# :nocov:
|
58
|
+
# :nocov: Re-executing the below in tests duplicates initializers and causes order-dependent failures.
|
44
59
|
ViewComponent::Base.prepend(ViewComponent::Instrumentation)
|
60
|
+
if app.config.view_component.use_deprecated_instrumentation_name
|
61
|
+
ViewComponent::Deprecation.deprecation_warning(
|
62
|
+
"!render.view_component",
|
63
|
+
"Use the new instrumentation key `render.view_component` instead. See https://viewcomponent.org/guide/instrumentation.html"
|
64
|
+
)
|
65
|
+
end
|
45
66
|
# :nocov:
|
46
67
|
end
|
47
68
|
end
|
48
69
|
end
|
49
70
|
|
71
|
+
# :nocov:
|
72
|
+
initializer "view_component.enable_capture_patch" do |app|
|
73
|
+
ActiveSupport.on_load(:view_component) do
|
74
|
+
ActionView::Base.include(ViewComponent::CaptureCompatibility) if app.config.view_component.capture_compatibility_patch_enabled
|
75
|
+
end
|
76
|
+
end
|
77
|
+
# :nocov:
|
78
|
+
|
50
79
|
initializer "view_component.set_autoload_paths" do |app|
|
51
80
|
options = app.config.view_component
|
52
81
|
|
@@ -65,6 +94,9 @@ module ViewComponent
|
|
65
94
|
initializer "view_component.monkey_patch_render" do |app|
|
66
95
|
next if Rails.version.to_f >= 6.1 || !app.config.view_component.render_monkey_patch_enabled
|
67
96
|
|
97
|
+
# :nocov:
|
98
|
+
ViewComponent::Deprecation.deprecation_warning("Monkey patching `render`", "ViewComponent 4.0 will remove the `render` monkey patch")
|
99
|
+
|
68
100
|
ActiveSupport.on_load(:action_view) do
|
69
101
|
require "view_component/render_monkey_patch"
|
70
102
|
ActionView::Base.prepend ViewComponent::RenderMonkeyPatch
|
@@ -76,11 +108,15 @@ module ViewComponent
|
|
76
108
|
ActionController::Base.prepend ViewComponent::RenderingMonkeyPatch
|
77
109
|
ActionController::Base.prepend ViewComponent::RenderToStringMonkeyPatch
|
78
110
|
end
|
111
|
+
# :nocov:
|
79
112
|
end
|
80
113
|
|
81
114
|
initializer "view_component.include_render_component" do |_app|
|
82
115
|
next if Rails.version.to_f >= 6.1
|
83
116
|
|
117
|
+
# :nocov:
|
118
|
+
ViewComponent::Deprecation.deprecation_warning("using `render_component`", "ViewComponent 4.0 will remove `render_component`")
|
119
|
+
|
84
120
|
ActiveSupport.on_load(:action_view) do
|
85
121
|
require "view_component/render_component_helper"
|
86
122
|
ActionView::Base.include ViewComponent::RenderComponentHelper
|
@@ -92,20 +128,21 @@ module ViewComponent
|
|
92
128
|
ActionController::Base.include ViewComponent::RenderingComponentHelper
|
93
129
|
ActionController::Base.include ViewComponent::RenderComponentToStringHelper
|
94
130
|
end
|
131
|
+
# :nocov:
|
95
132
|
end
|
96
133
|
|
97
134
|
initializer "static assets" do |app|
|
98
|
-
if app.config
|
135
|
+
if serve_static_preview_assets?(app.config)
|
99
136
|
app.middleware.use(::ActionDispatch::Static, "#{root}/app/assets/vendor")
|
100
137
|
end
|
101
138
|
end
|
102
139
|
|
140
|
+
def serve_static_preview_assets?(app_config)
|
141
|
+
app_config.view_component.show_previews && app_config.public_file_server.enabled
|
142
|
+
end
|
143
|
+
|
103
144
|
initializer "compiler mode" do |_app|
|
104
|
-
ViewComponent::Compiler.
|
105
|
-
ViewComponent::Compiler::DEVELOPMENT_MODE
|
106
|
-
else
|
107
|
-
ViewComponent::Compiler::PRODUCTION_MODE
|
108
|
-
end
|
145
|
+
ViewComponent::Compiler.development_mode = (Rails.env.development? || Rails.env.test?)
|
109
146
|
end
|
110
147
|
|
111
148
|
config.after_initialize do |app|
|
@@ -137,26 +174,19 @@ module ViewComponent
|
|
137
174
|
end
|
138
175
|
end
|
139
176
|
|
177
|
+
# :nocov:
|
178
|
+
if RUBY_VERSION < "3.2.0"
|
179
|
+
ViewComponent::Deprecation.deprecation_warning("Support for Ruby versions < 3.2.0", "ViewComponent v4 will remove support for Ruby versions < 3.2.0 no earlier than April 1, 2025")
|
180
|
+
end
|
181
|
+
|
182
|
+
if Rails.version.to_f < 7.1
|
183
|
+
ViewComponent::Deprecation.deprecation_warning("Support for Rails versions < 7.1", "ViewComponent v4 will remove support for Rails versions < 7.1 no earlier than April 1, 2025")
|
184
|
+
end
|
185
|
+
# :nocov:
|
186
|
+
|
140
187
|
app.executor.to_run :before do
|
141
188
|
CompileCache.invalidate! unless ActionView::Base.cache_template_loading
|
142
189
|
end
|
143
190
|
end
|
144
191
|
end
|
145
192
|
end
|
146
|
-
|
147
|
-
if RUBY_VERSION < "2.7.0"
|
148
|
-
ViewComponent::Deprecation.deprecation_warning("Support for Ruby versions < 2.7.0")
|
149
|
-
end
|
150
|
-
|
151
|
-
# :nocov:
|
152
|
-
unless defined?(ViewComponent::Base)
|
153
|
-
require "view_component/deprecation"
|
154
|
-
|
155
|
-
ViewComponent::Deprecation.deprecation_warning(
|
156
|
-
"Manually loading the engine",
|
157
|
-
"remove `require \"view_component/engine\"`"
|
158
|
-
)
|
159
|
-
|
160
|
-
require "view_component"
|
161
|
-
end
|
162
|
-
# :nocov:
|
@@ -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 during initialization 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 during initialization 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 during initialization, 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
|
@@ -80,13 +85,7 @@ module ViewComponent # :nodoc:
|
|
80
85
|
Dir["#{path}/#{preview_name}_preview/#{example}.html.*"].first
|
81
86
|
end
|
82
87
|
|
83
|
-
if preview_path.nil?
|
84
|
-
raise(
|
85
|
-
PreviewTemplateError,
|
86
|
-
"A preview template for example #{example} doesn't exist.\n\n" \
|
87
|
-
"To fix this issue, create a template for the example."
|
88
|
-
)
|
89
|
-
end
|
88
|
+
raise MissingPreviewTemplateError.new(example) if preview_path.nil?
|
90
89
|
|
91
90
|
path = Dir["#{preview_path}/#{preview_name}_preview/#{example}.html.*"].first
|
92
91
|
Pathname.new(path)
|
@@ -103,7 +102,7 @@ module ViewComponent # :nodoc:
|
|
103
102
|
|
104
103
|
def load_previews
|
105
104
|
Array(preview_paths).each do |preview_path|
|
106
|
-
Dir["#{preview_path}/**/*
|
105
|
+
Dir["#{preview_path}/**/*preview.rb"].sort.each { |file| require_dependency file }
|
107
106
|
end
|
108
107
|
end
|
109
108
|
|
@@ -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
|