view_component 3.23.2 → 4.0.0.alpha1
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 +2 -9
- data/app/controllers/view_components_system_test_controller.rb +17 -20
- data/app/views/test_mailer/test_asset_email.html.erb +1 -0
- data/app/views/view_components/preview.html.erb +1 -9
- data/docs/CHANGELOG.md +93 -9
- data/lib/view_component/base.rb +43 -66
- data/lib/view_component/collection.rb +11 -17
- data/lib/view_component/compiler.rb +50 -74
- data/lib/view_component/config.rb +0 -21
- data/lib/view_component/deprecation.rb +1 -1
- data/lib/view_component/engine.rb +3 -82
- data/lib/view_component/errors.rb +16 -22
- data/lib/view_component/inline_template.rb +2 -3
- data/lib/view_component/instrumentation.rb +4 -10
- data/lib/view_component/preview.rb +3 -10
- data/lib/view_component/request_details.rb +30 -0
- data/lib/view_component/slot.rb +2 -5
- data/lib/view_component/slotable.rb +30 -37
- data/lib/view_component/system_test_helpers.rb +1 -2
- data/lib/view_component/template.rb +106 -83
- data/lib/view_component/test_helpers.rb +22 -42
- data/lib/view_component/translatable.rb +24 -23
- data/lib/view_component/use_helpers.rb +3 -4
- data/lib/view_component/version.rb +4 -4
- data/lib/view_component.rb +0 -1
- metadata +68 -211
- data/app/assets/vendor/prism.css +0 -4
- data/app/assets/vendor/prism.min.js +0 -12
- data/app/helpers/preview_helper.rb +0 -85
- data/app/views/view_components/_preview_source.html.erb +0 -17
- data/lib/rails/generators/abstract_generator.rb +0 -56
- data/lib/rails/generators/component/component_generator.rb +0 -67
- data/lib/rails/generators/component/templates/component.rb.tt +0 -16
- data/lib/rails/generators/erb/component_generator.rb +0 -33
- data/lib/rails/generators/erb/templates/component.html.erb.tt +0 -1
- data/lib/rails/generators/haml/component_generator.rb +0 -22
- data/lib/rails/generators/haml/templates/component.html.haml.tt +0 -1
- data/lib/rails/generators/locale/component_generator.rb +0 -46
- data/lib/rails/generators/preview/component_generator.rb +0 -39
- data/lib/rails/generators/preview/templates/component_preview.rb.tt +0 -9
- data/lib/rails/generators/rspec/component_generator.rb +0 -31
- data/lib/rails/generators/rspec/templates/component_spec.rb.tt +0 -15
- data/lib/rails/generators/slim/component_generator.rb +0 -22
- data/lib/rails/generators/slim/templates/component.html.slim.tt +0 -1
- data/lib/rails/generators/stimulus/component_generator.rb +0 -44
- data/lib/rails/generators/stimulus/templates/component_controller.js.tt +0 -7
- data/lib/rails/generators/stimulus/templates/component_controller.ts.tt +0 -9
- data/lib/rails/generators/tailwindcss/component_generator.rb +0 -11
- data/lib/rails/generators/tailwindcss/templates/component.html.erb.tt +0 -1
- data/lib/rails/generators/test_unit/component_generator.rb +0 -20
- data/lib/rails/generators/test_unit/templates/component_test.rb.tt +0 -12
- data/lib/view_component/component_error.rb +0 -6
- data/lib/view_component/rails/tasks/view_component.rake +0 -20
- data/lib/view_component/render_component_helper.rb +0 -10
- data/lib/view_component/render_component_to_string_helper.rb +0 -9
- data/lib/view_component/render_monkey_patch.rb +0 -13
- data/lib/view_component/render_to_string_monkey_patch.rb +0 -13
- data/lib/view_component/rendering_component_helper.rb +0 -9
- data/lib/view_component/rendering_monkey_patch.rb +0 -13
@@ -12,12 +12,10 @@ module ViewComponent
|
|
12
12
|
singular: %i[content render].freeze,
|
13
13
|
plural: %i[contents renders].freeze
|
14
14
|
}.freeze
|
15
|
+
private_constant :RESERVED_NAMES
|
15
16
|
|
16
|
-
# Setup component slot state
|
17
17
|
included do
|
18
|
-
|
19
|
-
class_attribute :registered_slots
|
20
|
-
self.registered_slots = {}
|
18
|
+
class_attribute :registered_slots, default: {}
|
21
19
|
end
|
22
20
|
|
23
21
|
class_methods do
|
@@ -84,10 +82,9 @@ module ViewComponent
|
|
84
82
|
|
85
83
|
setter_method_name = :"with_#{slot_name}"
|
86
84
|
|
87
|
-
define_method setter_method_name do |*args, &block|
|
88
|
-
set_slot(slot_name, nil, *args, &block)
|
85
|
+
define_method setter_method_name do |*args, **kwargs, &block|
|
86
|
+
set_slot(slot_name, nil, *args, **kwargs, &block)
|
89
87
|
end
|
90
|
-
ruby2_keywords(setter_method_name) if respond_to?(:ruby2_keywords, true)
|
91
88
|
|
92
89
|
self::GeneratedSlotMethods.define_method slot_name do
|
93
90
|
get_slot(slot_name)
|
@@ -155,10 +152,9 @@ module ViewComponent
|
|
155
152
|
|
156
153
|
setter_method_name = :"with_#{singular_name}"
|
157
154
|
|
158
|
-
define_method setter_method_name do |*args, &block|
|
159
|
-
set_slot(slot_name, nil, *args, &block)
|
155
|
+
define_method setter_method_name do |*args, **kwargs, &block|
|
156
|
+
set_slot(slot_name, nil, *args, **kwargs, &block)
|
160
157
|
end
|
161
|
-
ruby2_keywords(setter_method_name) if respond_to?(:ruby2_keywords, true)
|
162
158
|
|
163
159
|
define_method :"with_#{singular_name}_content" do |content|
|
164
160
|
send(setter_method_name) { content.to_s }
|
@@ -215,6 +211,21 @@ module ViewComponent
|
|
215
211
|
super
|
216
212
|
end
|
217
213
|
|
214
|
+
# Called by the compiler, as instance methods are not defined when slots are first registered
|
215
|
+
def register_default_slots
|
216
|
+
registered_slots.each do |slot_name, config|
|
217
|
+
config[:default_method] = instance_methods.find { |method_name| method_name == :"default_#{slot_name}" }
|
218
|
+
|
219
|
+
registered_slots[slot_name] = config
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
private
|
224
|
+
|
225
|
+
def register_slot(slot_name, **kwargs)
|
226
|
+
registered_slots[slot_name] = define_slot(slot_name, **kwargs)
|
227
|
+
end
|
228
|
+
|
218
229
|
def register_polymorphic_slot(slot_name, types, collection:)
|
219
230
|
self::GeneratedSlotMethods.define_method(slot_name) do
|
220
231
|
get_slot(slot_name)
|
@@ -250,10 +261,9 @@ module ViewComponent
|
|
250
261
|
raise AlreadyDefinedPolymorphicSlotSetterError.new(setter_method_name, poly_slot_name)
|
251
262
|
end
|
252
263
|
|
253
|
-
define_method(setter_method_name) do |*args, &block|
|
254
|
-
set_polymorphic_slot(slot_name, poly_type, *args, &block)
|
264
|
+
define_method(setter_method_name) do |*args, **kwargs, &block|
|
265
|
+
set_polymorphic_slot(slot_name, poly_type, *args, **kwargs, &block)
|
255
266
|
end
|
256
|
-
ruby2_keywords(setter_method_name) if respond_to?(:ruby2_keywords, true)
|
257
267
|
|
258
268
|
define_method :"with_#{poly_slot_name}_content" do |content|
|
259
269
|
send(setter_method_name) { content.to_s }
|
@@ -268,21 +278,6 @@ module ViewComponent
|
|
268
278
|
}
|
269
279
|
end
|
270
280
|
|
271
|
-
# Called by the compiler, as instance methods are not defined when slots are first registered
|
272
|
-
def register_default_slots
|
273
|
-
registered_slots.each do |slot_name, config|
|
274
|
-
config[:default_method] = instance_methods.find { |method_name| method_name == :"default_#{slot_name}" }
|
275
|
-
|
276
|
-
registered_slots[slot_name] = config
|
277
|
-
end
|
278
|
-
end
|
279
|
-
|
280
|
-
private
|
281
|
-
|
282
|
-
def register_slot(slot_name, **kwargs)
|
283
|
-
registered_slots[slot_name] = define_slot(slot_name, **kwargs)
|
284
|
-
end
|
285
|
-
|
286
281
|
def define_slot(slot_name, collection:, callable:)
|
287
282
|
slot = {collection: collection}
|
288
283
|
return slot unless callable
|
@@ -370,7 +365,7 @@ module ViewComponent
|
|
370
365
|
end
|
371
366
|
end
|
372
367
|
|
373
|
-
def set_slot(slot_name, slot_definition = nil, *args, &block)
|
368
|
+
def set_slot(slot_name, slot_definition = nil, *args, **kwargs, &block)
|
374
369
|
slot_definition ||= self.class.registered_slots[slot_name]
|
375
370
|
slot = Slot.new(self)
|
376
371
|
|
@@ -387,11 +382,11 @@ module ViewComponent
|
|
387
382
|
|
388
383
|
# If class
|
389
384
|
if slot_definition[:renderable]
|
390
|
-
slot.__vc_component_instance = slot_definition[:renderable].new(*args)
|
385
|
+
slot.__vc_component_instance = slot_definition[:renderable].new(*args, **kwargs)
|
391
386
|
# If class name as a string
|
392
387
|
elsif slot_definition[:renderable_class_name]
|
393
388
|
slot.__vc_component_instance =
|
394
|
-
self.class.const_get(slot_definition[:renderable_class_name]).new(*args)
|
389
|
+
self.class.const_get(slot_definition[:renderable_class_name]).new(*args, **kwargs)
|
395
390
|
# If passed a lambda
|
396
391
|
elsif slot_definition[:renderable_function]
|
397
392
|
# Use `bind(self)` to ensure lambda is executed in the context of the
|
@@ -400,11 +395,11 @@ module ViewComponent
|
|
400
395
|
renderable_function = slot_definition[:renderable_function].bind(self)
|
401
396
|
renderable_value =
|
402
397
|
if block
|
403
|
-
renderable_function.call(*args) do |*rargs|
|
398
|
+
renderable_function.call(*args, **kwargs) do |*rargs|
|
404
399
|
view_context.capture(*rargs, &block)
|
405
400
|
end
|
406
401
|
else
|
407
|
-
renderable_function.call(*args)
|
402
|
+
renderable_function.call(*args, **kwargs)
|
408
403
|
end
|
409
404
|
|
410
405
|
# Function calls can return components, so if it's a component handle it specially
|
@@ -426,9 +421,8 @@ module ViewComponent
|
|
426
421
|
|
427
422
|
slot
|
428
423
|
end
|
429
|
-
ruby2_keywords(:set_slot) if respond_to?(:ruby2_keywords, true)
|
430
424
|
|
431
|
-
def set_polymorphic_slot(slot_name, poly_type = nil, *args, &block)
|
425
|
+
def set_polymorphic_slot(slot_name, poly_type = nil, *args, **kwargs, &block)
|
432
426
|
slot_definition = self.class.registered_slots[slot_name]
|
433
427
|
|
434
428
|
if !slot_definition[:collection] && defined?(@__vc_set_slots) && @__vc_set_slots[slot_name]
|
@@ -437,8 +431,7 @@ module ViewComponent
|
|
437
431
|
|
438
432
|
poly_def = slot_definition[:renderable_hash][poly_type]
|
439
433
|
|
440
|
-
set_slot(slot_name, poly_def, *args, &block)
|
434
|
+
set_slot(slot_name, poly_def, *args, **kwargs, &block)
|
441
435
|
end
|
442
|
-
ruby2_keywords(:set_polymorphic_slot) if respond_to?(:ruby2_keywords, true)
|
443
436
|
end
|
444
437
|
end
|
@@ -4,7 +4,6 @@ module ViewComponent
|
|
4
4
|
module SystemTestHelpers
|
5
5
|
include TestHelpers
|
6
6
|
|
7
|
-
#
|
8
7
|
# Returns a block that can be used to visit the path of the inline rendered component.
|
9
8
|
# @param fragment [Nokogiri::Fragment] The fragment returned from `render_inline`.
|
10
9
|
# @param layout [String] The (optional) layout to use.
|
@@ -18,7 +17,7 @@ module ViewComponent
|
|
18
17
|
file.write(vc_test_controller.render_to_string(html: fragment.to_html.html_safe, layout: layout))
|
19
18
|
file.rewind
|
20
19
|
|
21
|
-
|
20
|
+
yield("/_system_test_entrypoint?file=#{file.path.split("/").last}")
|
22
21
|
ensure
|
23
22
|
file.unlink
|
24
23
|
end
|
@@ -2,69 +2,115 @@
|
|
2
2
|
|
3
3
|
module ViewComponent
|
4
4
|
class Template
|
5
|
+
DEFAULT_FORMAT = :html
|
6
|
+
private_constant :DEFAULT_FORMAT
|
7
|
+
|
5
8
|
DataWithSource = Struct.new(:format, :identifier, :short_identifier, :type, keyword_init: true)
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
this_format: nil,
|
14
|
-
variant: nil,
|
15
|
-
lineno: nil,
|
16
|
-
path: nil,
|
17
|
-
extension: nil,
|
18
|
-
source: nil,
|
19
|
-
method_name: nil,
|
20
|
-
defined_on_self: true
|
21
|
-
)
|
9
|
+
|
10
|
+
attr_reader :details
|
11
|
+
|
12
|
+
delegate :virtual_path, to: :@component
|
13
|
+
delegate :format, :variant, to: :@details
|
14
|
+
|
15
|
+
def initialize(component:, details:, lineno: nil, path: nil)
|
22
16
|
@component = component
|
23
|
-
@
|
24
|
-
@this_format = this_format
|
25
|
-
@variant = variant&.to_sym
|
17
|
+
@details = details
|
26
18
|
@lineno = lineno
|
27
19
|
@path = path
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
20
|
+
end
|
21
|
+
|
22
|
+
class File < Template
|
23
|
+
def initialize(component:, details:, path:)
|
24
|
+
super(
|
25
|
+
component: component,
|
26
|
+
details: details,
|
27
|
+
path: path,
|
28
|
+
lineno: 0
|
29
|
+
)
|
30
|
+
end
|
31
|
+
|
32
|
+
def type
|
33
|
+
:file
|
34
|
+
end
|
35
|
+
|
36
|
+
# Load file each time we look up #source in case the file has been modified
|
37
|
+
def source
|
38
|
+
::File.read(@path)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
class Inline < Template
|
43
|
+
attr_reader :source
|
44
|
+
|
45
|
+
def initialize(component:, inline_template:)
|
46
|
+
details = ActionView::TemplateDetails.new(nil, inline_template.language.to_sym, nil, nil)
|
47
|
+
|
48
|
+
super(
|
49
|
+
component: component,
|
50
|
+
details: details,
|
51
|
+
path: inline_template.path,
|
52
|
+
lineno: inline_template.lineno,
|
53
|
+
)
|
54
|
+
|
55
|
+
@source = inline_template.source.dup
|
56
|
+
end
|
57
|
+
|
58
|
+
def type
|
59
|
+
:inline
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
class InlineCall < Template
|
64
|
+
def initialize(component:, method_name:, defined_on_self:)
|
65
|
+
variant = method_name.to_s.include?("call_") ? method_name.to_s.sub("call_", "").to_sym : nil
|
66
|
+
details = ActionView::TemplateDetails.new(nil, nil, nil, variant)
|
67
|
+
|
68
|
+
super(component: component, details: details)
|
69
|
+
|
70
|
+
@call_method_name = method_name
|
71
|
+
@defined_on_self = defined_on_self
|
72
|
+
end
|
73
|
+
|
74
|
+
def type
|
75
|
+
:inline_call
|
76
|
+
end
|
77
|
+
|
78
|
+
def compile_to_component
|
79
|
+
@component.define_method(safe_method_name, @component.instance_method(@call_method_name))
|
80
|
+
end
|
81
|
+
|
82
|
+
def safe_method_name_call
|
83
|
+
m = safe_method_name
|
84
|
+
proc do
|
85
|
+
maybe_escape_html(send(m)) do
|
86
|
+
Kernel.warn("WARNING: The #{self.class} component rendered HTML-unsafe output. " \
|
87
|
+
"The output will be automatically escaped, but you may want to investigate.")
|
88
|
+
end
|
43
89
|
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def defined_on_self?
|
93
|
+
@defined_on_self
|
94
|
+
end
|
44
95
|
end
|
45
96
|
|
46
97
|
def compile_to_component
|
47
|
-
|
48
|
-
@component.silence_redefinition_of_method(@call_method_name)
|
98
|
+
@component.silence_redefinition_of_method(call_method_name)
|
49
99
|
|
50
|
-
|
51
|
-
|
52
|
-
def #{
|
100
|
+
# rubocop:disable Style/EvalWithLocation
|
101
|
+
@component.class_eval <<~RUBY, @path, @lineno
|
102
|
+
def #{call_method_name}
|
53
103
|
#{compiled_source}
|
54
104
|
end
|
55
|
-
|
56
|
-
|
57
|
-
end
|
105
|
+
RUBY
|
106
|
+
# rubocop:enable Style/EvalWithLocation
|
58
107
|
|
59
108
|
@component.define_method(safe_method_name, @component.instance_method(@call_method_name))
|
60
109
|
end
|
61
110
|
|
62
111
|
def safe_method_name_call
|
63
|
-
|
64
|
-
|
65
|
-
"maybe_escape_html(#{safe_method_name}) " \
|
66
|
-
"{ Kernel.warn(\"WARNING: The #{@component} component rendered HTML-unsafe output. " \
|
67
|
-
"The output will be automatically escaped, but you may want to investigate.\") } "
|
112
|
+
m = safe_method_name
|
113
|
+
proc { send(m) }
|
68
114
|
end
|
69
115
|
|
70
116
|
def requires_compiled_superclass?
|
@@ -72,63 +118,40 @@ module ViewComponent
|
|
72
118
|
end
|
73
119
|
|
74
120
|
def inline_call?
|
75
|
-
|
76
|
-
end
|
77
|
-
|
78
|
-
def inline?
|
79
|
-
@type == :inline
|
121
|
+
type == :inline_call
|
80
122
|
end
|
81
123
|
|
82
124
|
def default_format?
|
83
|
-
|
125
|
+
format.nil? || format == DEFAULT_FORMAT
|
84
126
|
end
|
127
|
+
alias_method :html?, :default_format?
|
85
128
|
|
86
|
-
def
|
87
|
-
@
|
129
|
+
def call_method_name
|
130
|
+
@call_method_name ||=
|
131
|
+
["call", (normalized_variant_name if variant.present?), (format unless default_format?)]
|
132
|
+
.compact.join("_").to_sym
|
88
133
|
end
|
89
134
|
|
90
135
|
def safe_method_name
|
91
|
-
"_#{
|
136
|
+
"_#{call_method_name}_#{@component.name.underscore.gsub("/", "__")}"
|
92
137
|
end
|
93
138
|
|
94
139
|
def normalized_variant_name
|
95
|
-
|
96
|
-
end
|
97
|
-
|
98
|
-
def defined_on_self?
|
99
|
-
@defined_on_self
|
140
|
+
variant.to_s.gsub("-", "__")
|
100
141
|
end
|
101
142
|
|
102
143
|
private
|
103
144
|
|
104
|
-
def source
|
105
|
-
if @source_originally_nil
|
106
|
-
# Load file each time we look up #source in case the file has been modified
|
107
|
-
File.read(@path)
|
108
|
-
else
|
109
|
-
@source
|
110
|
-
end
|
111
|
-
end
|
112
|
-
|
113
145
|
def compiled_source
|
114
|
-
handler =
|
146
|
+
handler = details.handler_class
|
115
147
|
this_source = source
|
116
148
|
this_source.rstrip! if @component.strip_trailing_whitespace?
|
117
149
|
|
118
150
|
short_identifier = defined?(Rails.root) ? @path.sub("#{Rails.root}/", "") : @path
|
119
|
-
|
151
|
+
format = self.format || DEFAULT_FORMAT
|
152
|
+
type = ActionView::Template::Types[format]
|
120
153
|
|
121
|
-
|
122
|
-
handler.call(
|
123
|
-
DataWithSource.new(format: @this_format, identifier: @path, short_identifier: short_identifier, type: type),
|
124
|
-
this_source
|
125
|
-
)
|
126
|
-
# :nocov:
|
127
|
-
# TODO: Remove in v4
|
128
|
-
else
|
129
|
-
handler.call(DataNoSource.new(source: this_source, identifier: @path, type: type))
|
130
|
-
end
|
131
|
-
# :nocov:
|
154
|
+
handler.call(DataWithSource.new(format:, identifier: @path, short_identifier:, type:), this_source)
|
132
155
|
end
|
133
156
|
end
|
134
157
|
end
|
@@ -18,18 +18,7 @@ module ViewComponent
|
|
18
18
|
def assert_component_rendered
|
19
19
|
assert_selector("body")
|
20
20
|
end
|
21
|
-
rescue LoadError
|
22
|
-
# We don't have a test case for running an application without capybara installed.
|
23
|
-
# It's probably fine to leave this without coverage.
|
24
|
-
# :nocov:
|
25
|
-
if ENV["DEBUG"]
|
26
|
-
warn(
|
27
|
-
"WARNING in `ViewComponent::TestHelpers`: Add `capybara` " \
|
28
|
-
"to Gemfile to use Capybara assertions."
|
29
|
-
)
|
30
|
-
end
|
31
|
-
|
32
|
-
# :nocov:
|
21
|
+
rescue LoadError # We don't have a test case for running an application without capybara installed.
|
33
22
|
end
|
34
23
|
|
35
24
|
# Returns the result of a render_inline call.
|
@@ -46,21 +35,12 @@ module ViewComponent
|
|
46
35
|
# ```
|
47
36
|
#
|
48
37
|
# @param component [ViewComponent::Base, ViewComponent::Collection] The instance of the component to be rendered.
|
49
|
-
# @return [Nokogiri::
|
38
|
+
# @return [Nokogiri::HTML5]
|
50
39
|
def render_inline(component, **args, &block)
|
51
40
|
@page = nil
|
52
|
-
@rendered_content =
|
53
|
-
if Rails.version.to_f >= 6.1
|
54
|
-
vc_test_controller.view_context.render(component, args, &block)
|
55
|
-
|
56
|
-
# :nocov:
|
57
|
-
else
|
58
|
-
vc_test_controller.view_context.render_component(component, &block)
|
59
|
-
end
|
41
|
+
@rendered_content = vc_test_controller.view_context.render(component, args, &block)
|
60
42
|
|
61
|
-
|
62
|
-
|
63
|
-
Nokogiri::HTML.fragment(@rendered_content)
|
43
|
+
Nokogiri::HTML5.fragment(@rendered_content)
|
64
44
|
end
|
65
45
|
|
66
46
|
# `JSON.parse`-d component output.
|
@@ -91,7 +71,7 @@ module ViewComponent
|
|
91
71
|
# @param name [String] The name of the preview to be rendered.
|
92
72
|
# @param from [ViewComponent::Preview] The class of the preview to be rendered.
|
93
73
|
# @param params [Hash] Parameters to be passed to the preview.
|
94
|
-
# @return [Nokogiri::
|
74
|
+
# @return [Nokogiri::HTML5]
|
95
75
|
def render_preview(name, from: __vc_test_helpers_preview_class, params: {})
|
96
76
|
previews_controller = __vc_test_helpers_build_controller(Rails.application.config.view_component.preview_controller.constantize)
|
97
77
|
|
@@ -107,7 +87,7 @@ module ViewComponent
|
|
107
87
|
|
108
88
|
@rendered_content = result
|
109
89
|
|
110
|
-
Nokogiri::
|
90
|
+
Nokogiri::HTML5.fragment(@rendered_content)
|
111
91
|
end
|
112
92
|
|
113
93
|
# Execute the given block in the view context (using `instance_exec`).
|
@@ -121,12 +101,11 @@ module ViewComponent
|
|
121
101
|
#
|
122
102
|
# assert_text("Hello, World!")
|
123
103
|
# ```
|
124
|
-
def render_in_view_context(
|
104
|
+
def render_in_view_context(...)
|
125
105
|
@page = nil
|
126
|
-
@rendered_content = vc_test_controller.view_context.instance_exec(
|
127
|
-
Nokogiri::
|
106
|
+
@rendered_content = vc_test_controller.view_context.instance_exec(...)
|
107
|
+
Nokogiri::HTML5.fragment(@rendered_content)
|
128
108
|
end
|
129
|
-
ruby2_keywords(:render_in_view_context) if respond_to?(:ruby2_keywords, true)
|
130
109
|
|
131
110
|
# Set the Action Pack request variant for the given block:
|
132
111
|
#
|
@@ -136,11 +115,11 @@ module ViewComponent
|
|
136
115
|
# end
|
137
116
|
# ```
|
138
117
|
#
|
139
|
-
# @param
|
140
|
-
def with_variant(
|
118
|
+
# @param variants [Symbol[]] The variants to be set for the provided block.
|
119
|
+
def with_variant(*variants)
|
141
120
|
old_variants = vc_test_controller.view_context.lookup_context.variants
|
142
121
|
|
143
|
-
vc_test_controller.view_context.lookup_context.variants
|
122
|
+
vc_test_controller.view_context.lookup_context.variants += variants
|
144
123
|
yield
|
145
124
|
ensure
|
146
125
|
vc_test_controller.view_context.lookup_context.variants = old_variants
|
@@ -173,9 +152,14 @@ module ViewComponent
|
|
173
152
|
# end
|
174
153
|
# ```
|
175
154
|
#
|
176
|
-
# @param
|
177
|
-
def with_format(
|
178
|
-
|
155
|
+
# @param formats [Symbol[]] The format(s) to be set for the provided block.
|
156
|
+
def with_format(*formats)
|
157
|
+
old_formats = vc_test_controller.view_context.lookup_context.formats
|
158
|
+
|
159
|
+
vc_test_controller.view_context.lookup_context.formats = formats
|
160
|
+
yield
|
161
|
+
ensure
|
162
|
+
vc_test_controller.view_context.lookup_context.formats = old_formats
|
179
163
|
end
|
180
164
|
|
181
165
|
# Set the URL of the current request (such as when using request-dependent path helpers):
|
@@ -205,7 +189,7 @@ module ViewComponent
|
|
205
189
|
# @param full_path [String] The path to set for the current request.
|
206
190
|
# @param host [String] The host to set for the current request.
|
207
191
|
# @param method [String] The request method to set for the current request.
|
208
|
-
def with_request_url(full_path, host: nil, method: nil
|
192
|
+
def with_request_url(full_path, host: nil, method: nil)
|
209
193
|
old_request_host = vc_test_request.host
|
210
194
|
old_request_method = vc_test_request.request_method
|
211
195
|
old_request_path_info = vc_test_request.path_info
|
@@ -225,7 +209,6 @@ module ViewComponent
|
|
225
209
|
vc_test_request.set_header("action_dispatch.request.query_parameters",
|
226
210
|
Rack::Utils.parse_nested_query(query).with_indifferent_access)
|
227
211
|
vc_test_request.set_header(Rack::QUERY_STRING, query)
|
228
|
-
vc_test_request.format = format
|
229
212
|
yield
|
230
213
|
ensure
|
231
214
|
vc_test_request.host = old_request_host
|
@@ -284,11 +267,9 @@ module ViewComponent
|
|
284
267
|
|
285
268
|
def __vc_test_helpers_preview_class
|
286
269
|
result = if respond_to?(:described_class)
|
287
|
-
|
288
|
-
raise "`render_preview` expected a described_class, but it is nil." if described_class.nil?
|
270
|
+
raise ArgumentError.new("`render_preview` expected a described_class, but it is nil.") if described_class.nil?
|
289
271
|
|
290
272
|
"#{described_class}Preview"
|
291
|
-
# :nocov:
|
292
273
|
else
|
293
274
|
self.class.name.gsub("Test", "Preview")
|
294
275
|
end
|
@@ -296,6 +277,5 @@ module ViewComponent
|
|
296
277
|
rescue NameError
|
297
278
|
raise NameError, "`render_preview` expected to find #{result}, but it does not exist."
|
298
279
|
end
|
299
|
-
# :nocov:
|
300
280
|
end
|
301
281
|
end
|
@@ -1,7 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "erb"
|
4
|
-
require "set"
|
5
4
|
require "i18n"
|
6
5
|
require "active_support/concern"
|
7
6
|
|
@@ -10,19 +9,22 @@ module ViewComponent
|
|
10
9
|
extend ActiveSupport::Concern
|
11
10
|
|
12
11
|
HTML_SAFE_TRANSLATION_KEY = /(?:_|\b)html\z/
|
12
|
+
private_constant :HTML_SAFE_TRANSLATION_KEY
|
13
|
+
|
13
14
|
TRANSLATION_EXTENSIONS = %w[yml yaml].freeze
|
15
|
+
private_constant :TRANSLATION_EXTENSIONS
|
14
16
|
|
15
17
|
included do
|
16
|
-
class_attribute :
|
18
|
+
class_attribute :__vc_i18n_backend, instance_writer: false, instance_predicate: false
|
17
19
|
end
|
18
20
|
|
19
21
|
class_methods do
|
20
|
-
def
|
21
|
-
@
|
22
|
+
def __vc_i18n_scope
|
23
|
+
@__vc_i18n_scope ||= virtual_path.sub(%r{^/}, "").gsub(%r{/_?}, ".")
|
22
24
|
end
|
23
25
|
|
24
|
-
def
|
25
|
-
return if
|
26
|
+
def __vc_build_i18n_backend
|
27
|
+
return if __vc_compiled?
|
26
28
|
|
27
29
|
# We need to load the translations files from the ancestors so a component
|
28
30
|
# can inherit translations from its parent and is able to overwrite them.
|
@@ -33,31 +35,31 @@ module ViewComponent
|
|
33
35
|
end
|
34
36
|
|
35
37
|
# In development it will become nil if the translations file is removed
|
36
|
-
self.
|
38
|
+
self.__vc_i18n_backend = if translation_files.any?
|
37
39
|
I18nBackend.new(
|
38
|
-
|
40
|
+
scope: __vc_i18n_scope,
|
39
41
|
load_paths: translation_files
|
40
42
|
)
|
41
43
|
end
|
42
44
|
end
|
43
45
|
|
44
|
-
def
|
46
|
+
def __vc_i18n_key(key, scope = nil)
|
45
47
|
scope = scope.join(".") if scope.is_a? Array
|
46
48
|
key = key&.to_s unless key.is_a?(String)
|
47
49
|
key = "#{scope}.#{key}" if scope
|
48
|
-
key = "#{
|
50
|
+
key = "#{__vc_i18n_scope}#{key}" if key.start_with?(".")
|
49
51
|
key
|
50
52
|
end
|
51
53
|
|
52
54
|
def translate(key = nil, **options)
|
53
55
|
return key.map { |k| translate(k, **options) } if key.is_a?(Array)
|
54
56
|
|
55
|
-
|
57
|
+
__vc_ensure_compiled
|
56
58
|
|
57
59
|
locale = options.delete(:locale) || ::I18n.locale
|
58
|
-
key =
|
60
|
+
key = __vc_i18n_key(key, options.delete(:scope))
|
59
61
|
|
60
|
-
|
62
|
+
__vc_i18n_backend.translate(locale, key, options)
|
61
63
|
end
|
62
64
|
|
63
65
|
alias_method :t, :translate
|
@@ -66,8 +68,8 @@ module ViewComponent
|
|
66
68
|
class I18nBackend < ::I18n::Backend::Simple
|
67
69
|
EMPTY_HASH = {}.freeze
|
68
70
|
|
69
|
-
def initialize(
|
70
|
-
@
|
71
|
+
def initialize(scope:, load_paths:)
|
72
|
+
@__vc_i18n_scope = scope.split(".").map(&:to_sym)
|
71
73
|
@load_paths = load_paths
|
72
74
|
end
|
73
75
|
|
@@ -77,7 +79,7 @@ module ViewComponent
|
|
77
79
|
end
|
78
80
|
|
79
81
|
def scope_data(data)
|
80
|
-
@
|
82
|
+
@__vc_i18n_scope.reverse_each do |part|
|
81
83
|
data = {part => data}
|
82
84
|
end
|
83
85
|
data
|
@@ -91,19 +93,19 @@ module ViewComponent
|
|
91
93
|
def translate(key = nil, **options)
|
92
94
|
raise ViewComponent::TranslateCalledBeforeRenderError if view_context.nil?
|
93
95
|
|
94
|
-
return super unless
|
96
|
+
return super unless __vc_i18n_backend
|
95
97
|
return key.map { |k| translate(k, **options) } if key.is_a?(Array)
|
96
98
|
|
97
99
|
locale = options.delete(:locale) || ::I18n.locale
|
98
|
-
key = self.class.
|
100
|
+
key = self.class.__vc_i18n_key(key, options.delete(:scope))
|
99
101
|
as_html = HTML_SAFE_TRANSLATION_KEY.match?(key)
|
100
102
|
|
101
103
|
html_escape_translation_options!(options) if as_html
|
102
104
|
|
103
|
-
if key.start_with?(
|
105
|
+
if key.start_with?(__vc_i18n_scope + ".")
|
104
106
|
translated =
|
105
107
|
catch(:exception) do
|
106
|
-
|
108
|
+
__vc_i18n_backend.translate(locale, key, options)
|
107
109
|
end
|
108
110
|
|
109
111
|
# Fallback to the global translations
|
@@ -119,9 +121,8 @@ module ViewComponent
|
|
119
121
|
end
|
120
122
|
alias_method :t, :translate
|
121
123
|
|
122
|
-
|
123
|
-
|
124
|
-
self.class.i18n_scope
|
124
|
+
def __vc_i18n_scope
|
125
|
+
self.class.__vc_i18n_scope
|
125
126
|
end
|
126
127
|
|
127
128
|
private
|