view_component 4.20.0 → 4.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/docs/CHANGELOG.md +94 -0
- data/lib/generators/view_component/abstract_generator.rb +24 -4
- data/lib/generators/view_component/component/component_generator.rb +8 -6
- data/lib/generators/view_component/component/templates/component.rb.tt +1 -1
- data/lib/generators/view_component/erb/erb_generator.rb +2 -2
- data/lib/generators/view_component/haml/haml_generator.rb +1 -1
- data/lib/generators/view_component/locale/locale_generator.rb +2 -6
- data/lib/generators/view_component/preview/preview_generator.rb +9 -6
- data/lib/generators/view_component/preview/templates/component_preview.rb.tt +2 -2
- data/lib/generators/view_component/rspec/rspec_generator.rb +1 -1
- data/lib/generators/view_component/rspec/templates/component_spec.rb.tt +1 -1
- data/lib/generators/view_component/slim/slim_generator.rb +1 -1
- data/lib/generators/view_component/stimulus/stimulus_generator.rb +3 -7
- data/lib/generators/view_component/test_unit/templates/component_test.rb.tt +2 -2
- data/lib/generators/view_component/test_unit/test_unit_generator.rb +8 -7
- data/lib/view_component/base.rb +106 -98
- data/lib/view_component/cache_digest/dependency_tracking.rb +6 -6
- data/lib/view_component/cache_digest/digestor_lookup.rb +3 -1
- data/lib/view_component/cache_digest/resolver.rb +59 -38
- data/lib/view_component/cache_digest.rb +161 -53
- data/lib/view_component/cacheable.rb +16 -18
- data/lib/view_component/collection.rb +4 -9
- data/lib/view_component/compile_cache.rb +44 -5
- data/lib/view_component/compiler.rb +50 -2
- data/lib/view_component/config.rb +114 -8
- data/lib/view_component/engine.rb +1 -2
- data/lib/view_component/errors.rb +16 -0
- data/lib/view_component/slot.rb +12 -0
- data/lib/view_component/slotable.rb +28 -4
- data/lib/view_component/template.rb +16 -3
- data/lib/view_component/test_helpers.rb +38 -16
- data/lib/view_component/translatable.rb +34 -0
- data/lib/view_component/version.rb +1 -1
- metadata +1 -1
|
@@ -1,9 +1,102 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "view_component/
|
|
3
|
+
require "view_component/errors"
|
|
4
4
|
|
|
5
5
|
module ViewComponent
|
|
6
6
|
class Config
|
|
7
|
+
# An `ActiveSupport::OrderedOptions` that raises when an option it doesn't
|
|
8
|
+
# know about is set, rather than silently storing it.
|
|
9
|
+
class Options < ActiveSupport::OrderedOptions # :nodoc:
|
|
10
|
+
ROOT_NAMESPACE = "config.view_component"
|
|
11
|
+
|
|
12
|
+
def initialize(namespace, allowed_keys, default: nil, renamed: {}, removed: {})
|
|
13
|
+
super(default)
|
|
14
|
+
@namespace = namespace
|
|
15
|
+
@allowed_keys = allowed_keys
|
|
16
|
+
@renamed = renamed
|
|
17
|
+
@removed = removed
|
|
18
|
+
@nested_options = {}
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def []=(key, value)
|
|
22
|
+
key = key.to_sym
|
|
23
|
+
|
|
24
|
+
if @allowed_keys.include?(key)
|
|
25
|
+
@nested_options[key] ||= value if value.is_a?(Options)
|
|
26
|
+
if (nested_options = @nested_options[key])
|
|
27
|
+
raise TypeError, "#{@namespace}.#{key} must be a Hash" unless value.is_a?(Hash)
|
|
28
|
+
|
|
29
|
+
value = nested_options.dup.clear.tap do |options|
|
|
30
|
+
value.each_pair { |nested_key, nested_value| options[nested_key] = nested_value }
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
super
|
|
35
|
+
else
|
|
36
|
+
raise UnknownConfigOptionError.new(
|
|
37
|
+
"#{@namespace}.#{key}",
|
|
38
|
+
renamed_to: @renamed[key] && "#{ROOT_NAMESPACE}.#{@renamed[key]}",
|
|
39
|
+
removed: @removed.key?(key),
|
|
40
|
+
hint: @removed[key],
|
|
41
|
+
valid_options: @allowed_keys
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
alias_method :store, :[]=
|
|
47
|
+
|
|
48
|
+
def merge!(*others)
|
|
49
|
+
others.each do |other|
|
|
50
|
+
{}.merge!(other).each_pair do |key, value|
|
|
51
|
+
key = key.to_sym
|
|
52
|
+
value = yield(key, self[key], value) if block_given? && key?(key)
|
|
53
|
+
self[key] = value
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
self
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
alias_method :update, :merge!
|
|
60
|
+
|
|
61
|
+
def replace(other)
|
|
62
|
+
replacement = dup.clear
|
|
63
|
+
replacement.merge!(other)
|
|
64
|
+
super(replacement)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def transform_keys!(*args, &block)
|
|
68
|
+
return enum_for(:transform_keys!) if args.empty? && !block
|
|
69
|
+
|
|
70
|
+
replace(to_h.transform_keys!(*args, &block))
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def transform_values!(&block)
|
|
74
|
+
return enum_for(:transform_values!) unless block
|
|
75
|
+
|
|
76
|
+
replace(to_h.transform_values!(&block))
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Options renamed in 4.0, mapped to their new name.
|
|
81
|
+
RENAMED_OPTIONS = {
|
|
82
|
+
preview_controller: "previews.controller",
|
|
83
|
+
preview_route: "previews.route",
|
|
84
|
+
preview_paths: "previews.paths",
|
|
85
|
+
show_previews: "previews.enabled",
|
|
86
|
+
default_preview_layout: "previews.default_layout",
|
|
87
|
+
component_parent_class: "generate.parent_class",
|
|
88
|
+
view_component_path: "generate.path"
|
|
89
|
+
}.freeze
|
|
90
|
+
|
|
91
|
+
# Options removed in 4.0, mapped to a hint about what to do instead.
|
|
92
|
+
REMOVED_OPTIONS = {
|
|
93
|
+
test_controller: "Use the `vc_test_controller_class` test helper instead.",
|
|
94
|
+
render_monkey_patch_enabled: nil,
|
|
95
|
+
use_deprecated_instrumentation_name: "Events always use the `render.view_component` name.",
|
|
96
|
+
capture_compatibility_patch_enabled: nil,
|
|
97
|
+
show_previews_source: "Consider using Lookbook instead."
|
|
98
|
+
}.freeze
|
|
99
|
+
|
|
7
100
|
class << self
|
|
8
101
|
# `new` without any arguments initializes the default configuration, but
|
|
9
102
|
# it's important to differentiate in case that's no longer the case in
|
|
@@ -11,11 +104,16 @@ module ViewComponent
|
|
|
11
104
|
alias_method :default, :new
|
|
12
105
|
|
|
13
106
|
def defaults
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
previews
|
|
17
|
-
|
|
18
|
-
|
|
107
|
+
options = Options.new(
|
|
108
|
+
Options::ROOT_NAMESPACE,
|
|
109
|
+
%i[generate previews instrumentation_enabled],
|
|
110
|
+
renamed: RENAMED_OPTIONS,
|
|
111
|
+
removed: REMOVED_OPTIONS
|
|
112
|
+
)
|
|
113
|
+
options.generate = default_generate_options
|
|
114
|
+
options.previews = default_previews_options
|
|
115
|
+
options.instrumentation_enabled = false
|
|
116
|
+
options
|
|
19
117
|
end
|
|
20
118
|
|
|
21
119
|
# @!attribute generate
|
|
@@ -156,14 +254,22 @@ module ViewComponent
|
|
|
156
254
|
end
|
|
157
255
|
|
|
158
256
|
def default_generate_options
|
|
159
|
-
options =
|
|
257
|
+
options = Options.new(
|
|
258
|
+
"#{Options::ROOT_NAMESPACE}.generate",
|
|
259
|
+
%i[path preview_path parent_class sidecar stimulus_controller typescript locale distinct_locale_files preview use_component_path_for_rspec_tests],
|
|
260
|
+
default: false,
|
|
261
|
+
renamed: {component_parent_class: RENAMED_OPTIONS.fetch(:component_parent_class)}
|
|
262
|
+
)
|
|
160
263
|
options.preview_path = ""
|
|
161
264
|
options.path = "app/components"
|
|
162
265
|
options
|
|
163
266
|
end
|
|
164
267
|
|
|
165
268
|
def default_previews_options
|
|
166
|
-
options =
|
|
269
|
+
options = Options.new(
|
|
270
|
+
"#{Options::ROOT_NAMESPACE}.previews",
|
|
271
|
+
%i[controller route enabled default_layout paths]
|
|
272
|
+
)
|
|
167
273
|
options.controller = "ViewComponentsController"
|
|
168
274
|
options.route = "/rails/view_components"
|
|
169
275
|
options.enabled = defined?(Rails.env) && (Rails.env.development? || Rails.env.test?)
|
|
@@ -150,10 +150,9 @@ module ViewComponent
|
|
|
150
150
|
ActiveSupport.on_load(:action_view) do
|
|
151
151
|
next if ActionView::Base.cache_template_loading
|
|
152
152
|
|
|
153
|
-
template_watcher =
|
|
153
|
+
template_watcher = CompileCache.template_watcher(app.config.file_watcher)
|
|
154
154
|
|
|
155
155
|
app.executor.to_run :before do
|
|
156
|
-
template_watcher ||= CompileCache.template_watcher(app.config.file_watcher)
|
|
157
156
|
template_watcher.execute_if_updated
|
|
158
157
|
end
|
|
159
158
|
end
|
|
@@ -267,4 +267,20 @@ module ViewComponent
|
|
|
267
267
|
super(MESSAGE.gsub("COMPONENT", component_name.to_s))
|
|
268
268
|
end
|
|
269
269
|
end
|
|
270
|
+
|
|
271
|
+
class UnknownConfigOptionError < StandardError
|
|
272
|
+
def initialize(option, renamed_to: nil, removed: false, hint: nil, valid_options: [])
|
|
273
|
+
message =
|
|
274
|
+
if renamed_to
|
|
275
|
+
"`#{option}` was renamed to `#{renamed_to}` in ViewComponent 4.0."
|
|
276
|
+
elsif removed
|
|
277
|
+
"`#{option}` was removed in ViewComponent 4.0."
|
|
278
|
+
else
|
|
279
|
+
"`#{option}` isn't a ViewComponent configuration option. " \
|
|
280
|
+
"Valid options are: #{valid_options.join(", ")}."
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
super([message, hint].compact.join(" "))
|
|
284
|
+
end
|
|
285
|
+
end
|
|
270
286
|
end
|
data/lib/view_component/slot.rb
CHANGED
|
@@ -12,6 +12,18 @@ module ViewComponent
|
|
|
12
12
|
@parent = parent
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
+
# @private
|
|
16
|
+
def __vc_dup(parent)
|
|
17
|
+
dup.tap do |copy|
|
|
18
|
+
copy.instance_variable_set(:@parent, parent)
|
|
19
|
+
copy.remove_instance_variable(:@content) if defined?(@content)
|
|
20
|
+
if __vc_component_instance?
|
|
21
|
+
component = @__vc_component_instance
|
|
22
|
+
copy.__vc_component_instance = component.is_a?(Base) ? component.__vc_dup_for_render : component.dup
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
15
27
|
def content?
|
|
16
28
|
return true if defined?(@__vc_content) && @__vc_content.present?
|
|
17
29
|
return true if defined?(@__vc_content_set_by_with_content) && @__vc_content_set_by_with_content.present?
|
|
@@ -216,9 +216,9 @@ module ViewComponent
|
|
|
216
216
|
def __vc_register_default_slots
|
|
217
217
|
registered_slots.each do |slot_name, config|
|
|
218
218
|
default_method_name = :"default_#{slot_name}"
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
219
|
+
registered_slots[slot_name] = config.merge(
|
|
220
|
+
default_method: method_defined?(default_method_name) ? default_method_name : nil
|
|
221
|
+
)
|
|
222
222
|
end
|
|
223
223
|
end
|
|
224
224
|
|
|
@@ -361,6 +361,9 @@ module ViewComponent
|
|
|
361
361
|
|
|
362
362
|
# If there is a default method for the slot, call it
|
|
363
363
|
if (default_method = registered_slots[slot_name][:default_method])
|
|
364
|
+
@__vc_default_slots ||= {}
|
|
365
|
+
return @__vc_default_slots[slot_name] if @__vc_default_slots.key?(slot_name)
|
|
366
|
+
|
|
364
367
|
renderable_value = send(default_method)
|
|
365
368
|
slot = Slot.new(self)
|
|
366
369
|
|
|
@@ -370,13 +373,34 @@ module ViewComponent
|
|
|
370
373
|
slot.__vc_content = renderable_value
|
|
371
374
|
end
|
|
372
375
|
|
|
373
|
-
slot
|
|
376
|
+
@__vc_default_slots[slot_name] = slot
|
|
374
377
|
elsif self.class.registered_slots[slot_name][:collection]
|
|
375
378
|
# If empty slot is a collection, return an empty array
|
|
376
379
|
[]
|
|
377
380
|
end
|
|
378
381
|
end
|
|
379
382
|
|
|
383
|
+
# @private
|
|
384
|
+
# Collection spacers need independent slots whose parent is the new copy.
|
|
385
|
+
def __vc_dup_for_render
|
|
386
|
+
__vc_check_reused_instance!
|
|
387
|
+
|
|
388
|
+
dup.tap do |copy|
|
|
389
|
+
%i[@__vc_set_slots @__vc_default_slots].each do |variable|
|
|
390
|
+
next unless instance_variable_defined?(variable)
|
|
391
|
+
|
|
392
|
+
slots = instance_variable_get(variable).transform_values do |slot|
|
|
393
|
+
if slot.is_a?(Array)
|
|
394
|
+
slot.map { |item| item.__vc_dup(copy) }
|
|
395
|
+
else
|
|
396
|
+
slot.__vc_dup(copy)
|
|
397
|
+
end
|
|
398
|
+
end
|
|
399
|
+
copy.instance_variable_set(variable, slots)
|
|
400
|
+
end
|
|
401
|
+
end
|
|
402
|
+
end
|
|
403
|
+
|
|
380
404
|
def __vc_set_slot(slot_name, slot_definition = nil, *args, **kwargs, &block)
|
|
381
405
|
slot_definition ||= self.class.registered_slots[slot_name]
|
|
382
406
|
slot = Slot.new(self)
|
|
@@ -35,9 +35,9 @@ module ViewComponent
|
|
|
35
35
|
:file
|
|
36
36
|
end
|
|
37
37
|
|
|
38
|
-
#
|
|
38
|
+
# Only reuse file contents while eager loading; otherwise reflect edits.
|
|
39
39
|
def source
|
|
40
|
-
|
|
40
|
+
CompileCache.read_file(@path)
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
private
|
|
@@ -137,6 +137,19 @@ module ViewComponent
|
|
|
137
137
|
end
|
|
138
138
|
end
|
|
139
139
|
|
|
140
|
+
class Inherited < InlineCall
|
|
141
|
+
def initialize(component:, template:)
|
|
142
|
+
super(component: component, method_name: template.call_method_name, defined_on_self: false)
|
|
143
|
+
@details = template.details
|
|
144
|
+
@path = template.path
|
|
145
|
+
@template = template
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def compile_to_component
|
|
149
|
+
@component.define_method(safe_method_name, @component.instance_method(@template.safe_method_name))
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
140
153
|
def compile_to_component
|
|
141
154
|
@component.silence_redefinition_of_method(call_method_name)
|
|
142
155
|
|
|
@@ -184,7 +197,7 @@ module ViewComponent
|
|
|
184
197
|
end
|
|
185
198
|
|
|
186
199
|
def safe_method_name
|
|
187
|
-
"_#{call_method_name}_#{@component.name.underscore.gsub("/", "__")}"
|
|
200
|
+
"_#{call_method_name}_#{format}_#{@component.name.underscore.gsub("/", "__")}"
|
|
188
201
|
end
|
|
189
202
|
|
|
190
203
|
def normalized_variant_name
|
|
@@ -85,19 +85,21 @@ module ViewComponent
|
|
|
85
85
|
#
|
|
86
86
|
# In RSpec, `Preview` is appended to `described_class`.
|
|
87
87
|
def render_preview(name, from: __vc_test_helpers_preview_class, params: {})
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
88
|
+
@page = nil
|
|
89
|
+
controller_class = Rails.application.config.view_component.previews.controller.constantize
|
|
90
|
+
request = ActionDispatch::TestRequest.new(vc_test_request.env.dup)
|
|
91
|
+
query_parameters = vc_test_request.query_parameters.deep_dup.merge(params.stringify_keys)
|
|
92
|
+
request.path_parameters = vc_test_request.path_parameters.except(*params.keys.map(&:to_sym)).merge(
|
|
93
|
+
controller: controller_class.controller_path,
|
|
94
|
+
action: "previews",
|
|
95
|
+
path: "#{from.preview_name}/#{name}"
|
|
96
|
+
)
|
|
97
|
+
request.set_header("action_dispatch.request.query_parameters", query_parameters)
|
|
98
|
+
request.set_header(Rack::QUERY_STRING, query_parameters.to_query)
|
|
99
|
+
previews_controller = __vc_test_helpers_build_controller(controller_class, request: request)
|
|
97
100
|
previews_controller.set_response!(ActionDispatch::Response.new)
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
@rendered_content = result
|
|
101
|
+
previews_controller.process(:previews)
|
|
102
|
+
@rendered_content = previews_controller.response.body
|
|
101
103
|
|
|
102
104
|
Nokogiri::HTML5.fragment(@rendered_content)
|
|
103
105
|
end
|
|
@@ -149,11 +151,14 @@ module ViewComponent
|
|
|
149
151
|
# @param klass [Class<ActionController::Base>] The controller to be used.
|
|
150
152
|
def with_controller_class(klass)
|
|
151
153
|
old_controller = defined?(@vc_test_controller) && @vc_test_controller
|
|
154
|
+
old_view_context = defined?(@vc_test_view_context) && @vc_test_view_context
|
|
152
155
|
|
|
156
|
+
@vc_test_view_context = nil
|
|
153
157
|
@vc_test_controller = __vc_test_helpers_build_controller(klass)
|
|
154
158
|
yield
|
|
155
159
|
ensure
|
|
156
160
|
@vc_test_controller = old_controller
|
|
161
|
+
@vc_test_view_context = old_view_context
|
|
157
162
|
end
|
|
158
163
|
|
|
159
164
|
# Set format of the current request
|
|
@@ -211,15 +216,21 @@ module ViewComponent
|
|
|
211
216
|
# @param method [String] The request method to set for the current request.
|
|
212
217
|
# @param protocol [Symbol] The protocol to set for the current request (e.g., `:http` or `:https`).
|
|
213
218
|
def with_request_url(full_path, host: nil, method: nil, protocol: nil)
|
|
214
|
-
old_request_host = vc_test_request.
|
|
219
|
+
old_request_host = vc_test_request.get_header("HTTP_HOST")
|
|
215
220
|
old_request_method = vc_test_request.request_method
|
|
216
221
|
old_request_path_info = vc_test_request.path_info
|
|
217
222
|
old_request_path_parameters = vc_test_request.path_parameters
|
|
218
223
|
old_request_query_parameters = vc_test_request.query_parameters
|
|
219
224
|
old_request_query_string = vc_test_request.query_string
|
|
225
|
+
old_request_fullpath = vc_test_request.fullpath
|
|
226
|
+
old_request_original_fullpath = vc_test_request.original_fullpath
|
|
220
227
|
old_request_format = vc_test_request.format.symbol
|
|
221
228
|
old_request_scheme = vc_test_request.scheme
|
|
229
|
+
old_request_protocol = vc_test_request.instance_variable_get(:@protocol)
|
|
230
|
+
old_request_port = vc_test_request.instance_variable_get(:@port)
|
|
222
231
|
old_controller = defined?(@vc_test_controller) && @vc_test_controller
|
|
232
|
+
old_controller_params = old_controller.params if old_controller
|
|
233
|
+
old_controller_url_options = old_controller.instance_variable_get(:@_url_options) if old_controller
|
|
223
234
|
|
|
224
235
|
path, query = full_path.split("?", 2)
|
|
225
236
|
vc_test_request.instance_variable_set(:@fullpath, full_path)
|
|
@@ -227,21 +238,32 @@ module ViewComponent
|
|
|
227
238
|
vc_test_request.host = host if host
|
|
228
239
|
vc_test_request.request_method = method if method
|
|
229
240
|
vc_test_request.set_header(Rack::RACK_URL_SCHEME, protocol.to_s) if protocol
|
|
241
|
+
vc_test_request.instance_variable_set(:@protocol, nil)
|
|
242
|
+
vc_test_request.instance_variable_set(:@port, nil)
|
|
230
243
|
vc_test_request.path_info = path
|
|
231
244
|
vc_test_request.path_parameters = Rails.application.routes.recognize_path_with_request(vc_test_request, path, {})
|
|
232
245
|
vc_test_request.set_header("action_dispatch.request.query_parameters",
|
|
233
246
|
Rack::Utils.parse_nested_query(query).with_indifferent_access)
|
|
234
247
|
vc_test_request.set_header(Rack::QUERY_STRING, query)
|
|
248
|
+
old_controller.params = nil if old_controller
|
|
249
|
+
old_controller&.instance_variable_set(:@_url_options, nil)
|
|
235
250
|
yield
|
|
236
251
|
ensure
|
|
237
|
-
vc_test_request.
|
|
252
|
+
vc_test_request.set_header("HTTP_HOST", old_request_host)
|
|
238
253
|
vc_test_request.request_method = old_request_method
|
|
239
254
|
vc_test_request.set_header(Rack::RACK_URL_SCHEME, old_request_scheme)
|
|
255
|
+
vc_test_request.instance_variable_set(:@protocol, old_request_protocol)
|
|
256
|
+
vc_test_request.instance_variable_set(:@port, old_request_port)
|
|
240
257
|
vc_test_request.path_info = old_request_path_info
|
|
241
258
|
vc_test_request.path_parameters = old_request_path_parameters
|
|
242
259
|
vc_test_request.set_header("action_dispatch.request.query_parameters", old_request_query_parameters)
|
|
243
260
|
vc_test_request.set_header(Rack::QUERY_STRING, old_request_query_string)
|
|
261
|
+
vc_test_request.instance_variable_set(:@fullpath, old_request_fullpath)
|
|
262
|
+
vc_test_request.instance_variable_set(:@original_fullpath, old_request_original_fullpath)
|
|
244
263
|
vc_test_request.format = old_request_format
|
|
264
|
+
old_controller.params = old_controller_params if old_controller
|
|
265
|
+
@vc_test_controller&.instance_variable_set(:@_url_options, nil)
|
|
266
|
+
old_controller&.instance_variable_set(:@_url_options, old_controller_url_options)
|
|
245
267
|
@vc_test_controller = old_controller
|
|
246
268
|
end
|
|
247
269
|
|
|
@@ -298,8 +320,8 @@ module ViewComponent
|
|
|
298
320
|
# Note: We prefix private methods here to prevent collisions in consumer's tests.
|
|
299
321
|
private
|
|
300
322
|
|
|
301
|
-
def __vc_test_helpers_build_controller(klass)
|
|
302
|
-
klass.new.tap { |c| c.request =
|
|
323
|
+
def __vc_test_helpers_build_controller(klass, request: vc_test_request)
|
|
324
|
+
klass.new.tap { |c| c.request = request }.extend(Rails.application.routes.url_helpers)
|
|
303
325
|
end
|
|
304
326
|
|
|
305
327
|
def __vc_test_helpers_preview_class
|
|
@@ -14,6 +14,34 @@ module ViewComponent
|
|
|
14
14
|
TRANSLATION_EXTENSIONS = %w[yml yaml].freeze
|
|
15
15
|
private_constant :TRANSLATION_EXTENSIONS
|
|
16
16
|
|
|
17
|
+
# Like Rails' translation helper, use a sentinel that survives cache serialization.
|
|
18
|
+
MISSING_TRANSLATION = -(2**61)
|
|
19
|
+
private_constant :MISSING_TRANSLATION
|
|
20
|
+
|
|
21
|
+
def self.symbolic_defaults?(options)
|
|
22
|
+
default = options[:default]
|
|
23
|
+
default.is_a?(Symbol) || (default.is_a?(Array) && default.any? { |value| value.is_a?(Symbol) })
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.translate_with_defaults(translator, key, options)
|
|
27
|
+
defaults = options[:default].is_a?(Array) ? options[:default].compact : [options[:default]]
|
|
28
|
+
options = options.except(:default)
|
|
29
|
+
|
|
30
|
+
translated = translator.translate(key, **options, default: MISSING_TRANSLATION)
|
|
31
|
+
return translated unless translated == MISSING_TRANSLATION
|
|
32
|
+
|
|
33
|
+
defaults.each do |default|
|
|
34
|
+
translated = if default.is_a?(Symbol)
|
|
35
|
+
translator.translate(default, **options, default: MISSING_TRANSLATION)
|
|
36
|
+
else
|
|
37
|
+
translator.translate(key, **options, default: [default, MISSING_TRANSLATION])
|
|
38
|
+
end
|
|
39
|
+
return translated unless translated == MISSING_TRANSLATION
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
translator.translate(key, **options)
|
|
43
|
+
end
|
|
44
|
+
|
|
17
45
|
included do
|
|
18
46
|
class_attribute :__vc_i18n_backend, instance_writer: false, instance_predicate: false
|
|
19
47
|
end
|
|
@@ -53,9 +81,13 @@ module ViewComponent
|
|
|
53
81
|
|
|
54
82
|
def translate(key = nil, **options)
|
|
55
83
|
return key.map { |k| translate(k, **options) } if key.is_a?(Array)
|
|
84
|
+
symbolic_defaults = Translatable.symbolic_defaults?(options)
|
|
85
|
+
return ::I18n.translate(key, **options) if key.nil? && !symbolic_defaults
|
|
56
86
|
|
|
57
87
|
__vc_ensure_compiled
|
|
58
88
|
|
|
89
|
+
return Translatable.translate_with_defaults(self, key, options) if symbolic_defaults
|
|
90
|
+
|
|
59
91
|
locale = options.delete(:locale) || ::I18n.locale
|
|
60
92
|
key = __vc_i18n_key(key, options.delete(:scope))
|
|
61
93
|
|
|
@@ -99,6 +131,8 @@ module ViewComponent
|
|
|
99
131
|
|
|
100
132
|
return @view_context.translate(key, **options) unless __vc_i18n_backend
|
|
101
133
|
return key.map { |k| translate(k, **options) } if key.is_a?(Array)
|
|
134
|
+
return Translatable.translate_with_defaults(self, key, options) if Translatable.symbolic_defaults?(options)
|
|
135
|
+
return @view_context.translate(key, **options) if key.nil?
|
|
102
136
|
|
|
103
137
|
locale = options.delete(:locale) || ::I18n.locale
|
|
104
138
|
key = self.class.__vc_i18n_key(key, options.delete(:scope))
|