view_component 3.0.0.rc1 → 3.0.0.rc3

Sign up to get free protection for your applications and to get access to all the features.

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