view_component 3.0.0.rc1 → 3.0.0.rc3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of view_component might be problematic. Click here for more details.

@@ -21,7 +21,7 @@ module ViewComponent
21
21
  end
22
22
 
23
23
  def build_i18n_backend
24
- return if CompileCache.compiled? self
24
+ return if compiled?
25
25
 
26
26
  self.i18n_backend = if (translation_files = sidecar_files(%w[yml yaml])).any?
27
27
  # Returning nil cleans up if translations file has been removed since the last compilation
@@ -32,6 +32,27 @@ module ViewComponent
32
32
  )
33
33
  end
34
34
  end
35
+
36
+ def i18n_key(key, scope = nil)
37
+ scope = scope.join(".") if scope.is_a? Array
38
+ key = key&.to_s unless key.is_a?(String)
39
+ key = "#{scope}.#{key}" if scope
40
+ key = "#{i18n_scope}#{key}" if key.start_with?(".")
41
+ key
42
+ end
43
+
44
+ def translate(key = nil, **options)
45
+ return key.map { |k| translate(k, **options) } if key.is_a?(Array)
46
+
47
+ ensure_compiled
48
+
49
+ locale = options.delete(:locale) || ::I18n.locale
50
+ key = i18n_key(key, options.delete(:scope))
51
+
52
+ i18n_backend.translate(locale, key, options)
53
+ end
54
+
55
+ alias_method :t, :translate
35
56
  end
36
57
 
37
58
  class I18nBackend < ::I18n::Backend::Simple
@@ -64,15 +85,10 @@ module ViewComponent
64
85
  return key.map { |k| translate(k, **options) } if key.is_a?(Array)
65
86
 
66
87
  locale = options.delete(:locale) || ::I18n.locale
67
- scope = options.delete(:scope)
68
- scope = scope.join(".") if scope.is_a? Array
69
- key = key&.to_s unless key.is_a?(String)
70
- key = "#{scope}.#{key}" if scope
71
- key = "#{i18n_scope}#{key}" if key.start_with?(".")
72
-
73
- if HTML_SAFE_TRANSLATION_KEY.match?(key)
74
- html_escape_translation_options!(options)
75
- end
88
+ key = self.class.i18n_key(key, options.delete(:scope))
89
+ as_html = HTML_SAFE_TRANSLATION_KEY.match?(key)
90
+
91
+ html_escape_translation_options!(options) if as_html
76
92
 
77
93
  if key.start_with?(i18n_scope + ".")
78
94
  translated =
@@ -85,10 +101,7 @@ module ViewComponent
85
101
  return super(key, locale: locale, **options)
86
102
  end
87
103
 
88
- if HTML_SAFE_TRANSLATION_KEY.match?(key)
89
- translated = html_safe_translation(translated)
90
- end
91
-
104
+ translated = html_safe_translation(translated) if as_html
92
105
  translated
93
106
  else
94
107
  super(key, locale: locale, **options)
@@ -101,6 +114,8 @@ module ViewComponent
101
114
  self.class.i18n_scope
102
115
  end
103
116
 
117
+ private
118
+
104
119
  def html_safe_translation(translation)
105
120
  if translation.respond_to?(:map)
106
121
  translation.map { |element| html_safe_translation(element) }
@@ -112,18 +127,13 @@ module ViewComponent
112
127
  end
113
128
  end
114
129
 
115
- private
116
-
117
130
  def html_escape_translation_options!(options)
118
131
  options.each do |name, value|
119
- unless i18n_option?(name) || (name == :count && value.is_a?(Numeric))
120
- options[name] = ERB::Util.html_escape(value.to_s)
121
- end
122
- end
123
- end
132
+ next if ::I18n.reserved_keys_pattern.match?(name)
133
+ next if name == :count && value.is_a?(Numeric)
124
134
 
125
- def i18n_option?(name)
126
- (@i18n_option_names ||= I18n::RESERVED_KEYS.to_set).include?(name)
135
+ options[name] = ERB::Util.html_escape(value.to_s)
136
+ end
127
137
  end
128
138
  end
129
139
  end
@@ -5,7 +5,7 @@ module ViewComponent
5
5
  MAJOR = 3
6
6
  MINOR = 0
7
7
  PATCH = 0
8
- PRE = "rc1"
8
+ PRE = "rc3"
9
9
 
10
10
  STRING = [MAJOR, MINOR, PATCH, PRE].compact.join(".")
11
11
  end
@@ -7,11 +7,13 @@ module ViewComponent
7
7
  extend ActiveSupport::Autoload
8
8
 
9
9
  autoload :Base
10
+ autoload :CaptureCompatibility
10
11
  autoload :Compiler
11
12
  autoload :CompileCache
12
13
  autoload :ComponentError
13
14
  autoload :Config
14
15
  autoload :Deprecation
16
+ autoload :InlineTemplate
15
17
  autoload :Instrumentation
16
18
  autoload :Preview
17
19
  autoload :PreviewTemplateError
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: view_component
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.0.rc1
4
+ version: 3.0.0.rc3
5
5
  platform: ruby
6
6
  authors:
7
7
  - ViewComponent Team
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-01-17 00:00:00.000000000 Z
11
+ date: 2023-03-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -353,6 +353,7 @@ files:
353
353
  - lib/rails/generators/test_unit/templates/component_test.rb.tt
354
354
  - lib/view_component.rb
355
355
  - lib/view_component/base.rb
356
+ - lib/view_component/capture_compatibility.rb
356
357
  - lib/view_component/collection.rb
357
358
  - lib/view_component/compile_cache.rb
358
359
  - lib/view_component/compiler.rb
@@ -362,8 +363,8 @@ files:
362
363
  - lib/view_component/docs_builder_component.html.erb
363
364
  - lib/view_component/docs_builder_component.rb
364
365
  - lib/view_component/engine.rb
366
+ - lib/view_component/inline_template.rb
365
367
  - lib/view_component/instrumentation.rb
366
- - lib/view_component/polymorphic_slots.rb
367
368
  - lib/view_component/preview.rb
368
369
  - lib/view_component/preview_template_error.rb
369
370
  - lib/view_component/rails/tasks/view_component.rake
@@ -373,9 +374,8 @@ files:
373
374
  - lib/view_component/render_to_string_monkey_patch.rb
374
375
  - lib/view_component/rendering_component_helper.rb
375
376
  - lib/view_component/rendering_monkey_patch.rb
376
- - lib/view_component/slot_v2.rb
377
+ - lib/view_component/slot.rb
377
378
  - lib/view_component/slotable.rb
378
- - lib/view_component/slotable_v2.rb
379
379
  - lib/view_component/system_test_case.rb
380
380
  - lib/view_component/system_test_helpers.rb
381
381
  - lib/view_component/template_error.rb
@@ -1,91 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ViewComponent
4
- module PolymorphicSlots
5
- # In older rails versions, using a concern isn't a good idea here because they appear to not work with
6
- # Module#prepend and class methods.
7
- def self.included(base)
8
- if base != ViewComponent::Base
9
- # :nocov:
10
- location = Kernel.caller_locations(1, 1)[0]
11
-
12
- warn(
13
- "warning: ViewComponent::PolymorphicSlots is now included in ViewComponent::Base by default " \
14
- "and can be removed from #{location.path}:#{location.lineno}"
15
- )
16
- # :nocov:
17
- end
18
-
19
- base.singleton_class.prepend(ClassMethods)
20
- base.include(InstanceMethods)
21
- end
22
-
23
- module ClassMethods
24
- def renders_one(slot_name, callable = nil)
25
- return super unless callable.is_a?(Hash) && callable.key?(:types)
26
-
27
- validate_singular_slot_name(slot_name)
28
- register_polymorphic_slot(slot_name, callable[:types], collection: false)
29
- end
30
-
31
- def renders_many(slot_name, callable = nil)
32
- return super unless callable.is_a?(Hash) && callable.key?(:types)
33
-
34
- validate_plural_slot_name(slot_name)
35
- register_polymorphic_slot(slot_name, callable[:types], collection: true)
36
- end
37
-
38
- def register_polymorphic_slot(slot_name, types, collection:)
39
- unless types.empty?
40
- getter_name = slot_name
41
-
42
- define_method(getter_name) do
43
- get_slot(slot_name)
44
- end
45
-
46
- define_method("#{getter_name}?") do
47
- get_slot(slot_name).present?
48
- end
49
- end
50
-
51
- renderable_hash = types.each_with_object({}) do |(poly_type, poly_callable), memo|
52
- memo[poly_type] = define_slot(
53
- "#{slot_name}_#{poly_type}", collection: collection, callable: poly_callable
54
- )
55
-
56
- setter_name =
57
- if collection
58
- "#{ActiveSupport::Inflector.singularize(slot_name)}_#{poly_type}"
59
- else
60
- "#{slot_name}_#{poly_type}"
61
- end
62
-
63
- define_method("with_#{setter_name}") do |*args, &block|
64
- set_polymorphic_slot(slot_name, poly_type, *args, &block)
65
- end
66
- ruby2_keywords(:"with_#{setter_name}") if respond_to?(:ruby2_keywords, true)
67
- end
68
-
69
- registered_slots[slot_name] = {
70
- collection: collection,
71
- renderable_hash: renderable_hash
72
- }
73
- end
74
- end
75
-
76
- module InstanceMethods
77
- def set_polymorphic_slot(slot_name, poly_type = nil, *args, &block)
78
- slot_definition = self.class.registered_slots[slot_name]
79
-
80
- if !slot_definition[:collection] && (defined?(@__vc_set_slots) && @__vc_set_slots[slot_name])
81
- raise ArgumentError, "content for slot '#{slot_name}' has already been provided"
82
- end
83
-
84
- poly_def = slot_definition[:renderable_hash][poly_type]
85
-
86
- set_slot(slot_name, poly_def, *args, &block)
87
- end
88
- ruby2_keywords(:set_polymorphic_slot) if respond_to?(:ruby2_keywords, true)
89
- end
90
- end
91
- end
@@ -1,336 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "active_support/concern"
4
- require "view_component/slot_v2"
5
-
6
- module ViewComponent
7
- module SlotableV2
8
- extend ActiveSupport::Concern
9
-
10
- RESERVED_NAMES = {
11
- singular: %i[content render].freeze,
12
- plural: %i[contents renders].freeze
13
- }.freeze
14
-
15
- # Setup component slot state
16
- included do
17
- # Hash of registered Slots
18
- class_attribute :registered_slots
19
- self.registered_slots = {}
20
- end
21
-
22
- class_methods do
23
- ##
24
- # Registers a sub-component
25
- #
26
- # = Example
27
- #
28
- # renders_one :header -> (classes:) do
29
- # HeaderComponent.new(classes: classes)
30
- # end
31
- #
32
- # # OR
33
- #
34
- # renders_one :header, HeaderComponent
35
- #
36
- # where `HeaderComponent` is defined as:
37
- #
38
- # class HeaderComponent < ViewComponent::Base
39
- # def initialize(classes:)
40
- # @classes = classes
41
- # end
42
- # end
43
- #
44
- # and has the following template:
45
- #
46
- # <header class="<%= @classes %>">
47
- # <%= content %>
48
- # </header>
49
- #
50
- # = Rendering sub-component content
51
- #
52
- # The component's sidecar template can access the sub-component by calling a
53
- # helper method with the same name as the sub-component.
54
- #
55
- # <h1>
56
- # <%= header do %>
57
- # My header title
58
- # <% end %>
59
- # </h1>
60
- #
61
- # = Setting sub-component content
62
- #
63
- # Consumers of the component can render a sub-component by calling a
64
- # helper method with the same name as the slot prefixed with `with_`.
65
- #
66
- # <%= render_inline(MyComponent.new) do |component| %>
67
- # <% component.with_header(classes: "Foo") do %>
68
- # <p>Bar</p>
69
- # <% end %>
70
- # <% end %>
71
- def renders_one(slot_name, callable = nil)
72
- validate_singular_slot_name(slot_name)
73
- validate_plural_slot_name(ActiveSupport::Inflector.pluralize(slot_name).to_sym)
74
-
75
- define_method :"with_#{slot_name}" do |*args, &block|
76
- set_slot(slot_name, nil, *args, &block)
77
- end
78
- ruby2_keywords(:"with_#{slot_name}") if respond_to?(:ruby2_keywords, true)
79
-
80
- define_method slot_name do |*args, &block|
81
- get_slot(slot_name)
82
- end
83
- ruby2_keywords(slot_name.to_sym) if respond_to?(:ruby2_keywords, true)
84
-
85
- define_method "#{slot_name}?" do
86
- get_slot(slot_name).present?
87
- end
88
-
89
- register_slot(slot_name, collection: false, callable: callable)
90
- end
91
-
92
- ##
93
- # Registers a collection sub-component
94
- #
95
- # = Example
96
- #
97
- # renders_many :items, -> (name:) { ItemComponent.new(name: name }
98
- #
99
- # # OR
100
- #
101
- # renders_many :items, ItemComponent
102
- #
103
- # = Rendering sub-components
104
- #
105
- # The component's sidecar template can access the slot by calling a
106
- # helper method with the same name as the slot.
107
- #
108
- # <h1>
109
- # <% items.each do |item| %>
110
- # <%= item %>
111
- # <% end %>
112
- # </h1>
113
- #
114
- # = Setting sub-component content
115
- #
116
- # Consumers of the component can set the content of a slot by calling a
117
- # helper method with the same name as the slot prefixed with `with_`. The
118
- # method can be called multiple times to append to the slot.
119
- #
120
- # <%= render_inline(MyComponent.new) do |component| %>
121
- # <% component.with_item(name: "Foo") do %>
122
- # <p>One</p>
123
- # <% end %>
124
- #
125
- # <% component.with_item(name: "Bar") do %>
126
- # <p>two</p>
127
- # <% end %>
128
- # <% end %>
129
- def renders_many(slot_name, callable = nil)
130
- singular_name = ActiveSupport::Inflector.singularize(slot_name)
131
- validate_plural_slot_name(slot_name)
132
- validate_singular_slot_name(ActiveSupport::Inflector.singularize(slot_name).to_sym)
133
-
134
- define_method :"with_#{singular_name}" do |*args, &block|
135
- set_slot(slot_name, nil, *args, &block)
136
- end
137
- ruby2_keywords(:"with_#{singular_name}") if respond_to?(:ruby2_keywords, true)
138
-
139
- define_method :"with_#{slot_name}" do |collection_args = nil, &block|
140
- collection_args.map do |args|
141
- set_slot(slot_name, nil, **args, &block)
142
- end
143
- end
144
-
145
- define_method slot_name do |collection_args = nil, &block|
146
- get_slot(slot_name)
147
- end
148
-
149
- define_method "#{slot_name}?" do
150
- get_slot(slot_name).present?
151
- end
152
-
153
- register_slot(slot_name, collection: true, callable: callable)
154
- end
155
-
156
- def slot_type(slot_name)
157
- registered_slot = registered_slots[slot_name]
158
- if registered_slot
159
- registered_slot[:collection] ? :collection : :single
160
- else
161
- plural_slot_name = ActiveSupport::Inflector.pluralize(slot_name).to_sym
162
- plural_registered_slot = registered_slots[plural_slot_name]
163
- plural_registered_slot&.fetch(:collection) ? :collection_item : nil
164
- end
165
- end
166
-
167
- # Clone slot configuration into child class
168
- # see #test_slots_pollution
169
- def inherited(child)
170
- child.registered_slots = registered_slots.clone
171
- super
172
- end
173
-
174
- private
175
-
176
- def register_slot(slot_name, **kwargs)
177
- registered_slots[slot_name] = define_slot(slot_name, **kwargs)
178
- end
179
-
180
- def define_slot(slot_name, collection:, callable:)
181
- # Setup basic slot data
182
- slot = {
183
- collection: collection
184
- }
185
- return slot unless callable
186
-
187
- # If callable responds to `render_in`, we set it on the slot as a renderable
188
- if callable.respond_to?(:method_defined?) && callable.method_defined?(:render_in)
189
- slot[:renderable] = callable
190
- elsif callable.is_a?(String)
191
- # If callable is a string, we assume it's referencing an internal class
192
- slot[:renderable_class_name] = callable
193
- elsif callable.respond_to?(:call)
194
- # If slot doesn't respond to `render_in`, we assume it's a proc,
195
- # define a method, and save a reference to it to call when setting
196
- method_name = :"_call_#{slot_name}"
197
- define_method method_name, &callable
198
- slot[:renderable_function] = instance_method(method_name)
199
- else
200
- raise(
201
- ArgumentError,
202
- "invalid slot definition. Please pass a class, string, or callable (i.e. proc, lambda, etc)"
203
- )
204
- end
205
-
206
- slot
207
- end
208
-
209
- def validate_plural_slot_name(slot_name)
210
- if RESERVED_NAMES[:plural].include?(slot_name.to_sym)
211
- raise ArgumentError.new(
212
- "#{self} declares a slot named #{slot_name}, which is a reserved word in the ViewComponent framework.\n\n" \
213
- "To fix this issue, choose a different name."
214
- )
215
- end
216
-
217
- raise_if_slot_ends_with_question_mark(slot_name)
218
- raise_if_slot_registered(slot_name)
219
- end
220
-
221
- def validate_singular_slot_name(slot_name)
222
- if slot_name.to_sym == :content
223
- raise ArgumentError.new(
224
- "#{self} declares a slot named content, which is a reserved word in ViewComponent.\n\n" \
225
- "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" \
226
- "To fix this issue, either use the `content` accessor directly or choose a different slot name."
227
- )
228
- end
229
-
230
- if RESERVED_NAMES[:singular].include?(slot_name.to_sym)
231
- raise ArgumentError.new(
232
- "#{self} declares a slot named #{slot_name}, which is a reserved word in the ViewComponent framework.\n\n" \
233
- "To fix this issue, choose a different name."
234
- )
235
- end
236
-
237
- raise_if_slot_ends_with_question_mark(slot_name)
238
- raise_if_slot_registered(slot_name)
239
- end
240
-
241
- def raise_if_slot_registered(slot_name)
242
- if registered_slots.key?(slot_name)
243
- # TODO remove? This breaks overriding slots when slots are inherited
244
- raise ArgumentError.new(
245
- "#{self} declares the #{slot_name} slot multiple times.\n\n" \
246
- "To fix this issue, choose a different slot name."
247
- )
248
- end
249
- end
250
-
251
- def raise_if_slot_ends_with_question_mark(slot_name)
252
- if slot_name.to_s.ends_with?("?")
253
- raise ArgumentError.new(
254
- "#{self} declares a slot named #{slot_name}, which ends with a question mark.\n\n" \
255
- "This is not allowed because the ViewComponent framework already provides predicate " \
256
- "methods ending in `?`.\n\n" \
257
- "To fix this issue, choose a different name."
258
- )
259
- end
260
- end
261
- end
262
-
263
- def get_slot(slot_name)
264
- content unless content_evaluated? # ensure content is loaded so slots will be defined
265
-
266
- slot = self.class.registered_slots[slot_name]
267
- @__vc_set_slots ||= {}
268
-
269
- if @__vc_set_slots[slot_name]
270
- return @__vc_set_slots[slot_name]
271
- end
272
-
273
- if slot[:collection]
274
- []
275
- end
276
- end
277
-
278
- def set_slot(slot_name, slot_definition = nil, *args, &block)
279
- slot_definition ||= self.class.registered_slots[slot_name]
280
- slot = SlotV2.new(self)
281
-
282
- # Passing the block to the sub-component wrapper like this has two
283
- # benefits:
284
- #
285
- # 1. If this is a `content_area` style sub-component, we will render the
286
- # block via the `slot`
287
- #
288
- # 2. Since we've to pass block content to components when calling
289
- # `render`, evaluating the block here would require us to call
290
- # `view_context.capture` twice, which is slower
291
- slot.__vc_content_block = block if block
292
-
293
- # If class
294
- if slot_definition[:renderable]
295
- slot.__vc_component_instance = slot_definition[:renderable].new(*args)
296
- # If class name as a string
297
- elsif slot_definition[:renderable_class_name]
298
- slot.__vc_component_instance =
299
- self.class.const_get(slot_definition[:renderable_class_name]).new(*args)
300
- # If passed a lambda
301
- elsif slot_definition[:renderable_function]
302
- # Use `bind(self)` to ensure lambda is executed in the context of the
303
- # current component. This is necessary to allow the lambda to access helper
304
- # methods like `content_tag` as well as parent component state.
305
- renderable_function = slot_definition[:renderable_function].bind(self)
306
- renderable_value =
307
- if block
308
- renderable_function.call(*args) do |*rargs|
309
- view_context.capture(*rargs, &block)
310
- end
311
- else
312
- renderable_function.call(*args)
313
- end
314
-
315
- # Function calls can return components, so if it's a component handle it specially
316
- if renderable_value.respond_to?(:render_in)
317
- slot.__vc_component_instance = renderable_value
318
- else
319
- slot.__vc_content = renderable_value
320
- end
321
- end
322
-
323
- @__vc_set_slots ||= {}
324
-
325
- if slot_definition[:collection]
326
- @__vc_set_slots[slot_name] ||= []
327
- @__vc_set_slots[slot_name].push(slot)
328
- else
329
- @__vc_set_slots[slot_name] = slot
330
- end
331
-
332
- slot
333
- end
334
- ruby2_keywords(:set_slot) if respond_to?(:ruby2_keywords, true)
335
- end
336
- end