view_component 2.26.0 → 2.30.0

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.

checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5a1fb023bfaa4a1ea26166ddd22b5fcd4a7e5f5da1bc7f10039c2bc557f1a94c
4
- data.tar.gz: 5274b49f0e18f521b15181c70aeb4edcf3458cba13dc8c8ea59b9331d26681c5
3
+ metadata.gz: 6bb3c2781d5f07f404e5bf9582758537704a05c3937529c250ae657541d4f664
4
+ data.tar.gz: 0efa6d2e3bbf03ed206e02b9dad11bea1f5649ab655dfff78d1f48c68414129f
5
5
  SHA512:
6
- metadata.gz: fc60826197c5dbd54ebd53517ff59eab097449913802a4e17df9db33ab54d1daf8ff2e00d1629058962c267274ec7fe6e362aa036206d379e27c7c832db7d05c
7
- data.tar.gz: b3a65f7a829d9e9fc675904d61c6d1c1015c194c5a7abe596961b3cb20a3a4c2045f1ce49b1b98a551c4b745688c38610d3a86040c1f7f0229c3b247a51f9c65
6
+ metadata.gz: 9222381535158cc047e33e37341329c232ac984ee3f6895e95251cd9dabc9b92baebb69b336500932c264b48eb7f74ef0b7d3f918473d8fe83151ad73c18323b
7
+ data.tar.gz: 4da1bd8980affd316288695a52e9fcdccad912f3447081f0186194190ad48e52d397f36c34b3bea70ea743d32f2eda8d37779ecb588b50cbd626f7d89a4f365c
data/CHANGELOG.md CHANGED
@@ -2,6 +2,67 @@
2
2
 
3
3
  ## main
4
4
 
5
+ ## 2.30.0
6
+
7
+ * Deprecate `with_content_areas` in favor of [slots](https://viewcomponent.org/guide/slots.html).
8
+
9
+ *Joel Hawksley*
10
+
11
+ ## 2.29.0
12
+
13
+ * Allow Slot lambdas to share data from the parent component and allow chaining on the returned component.
14
+
15
+ *Sjors Baltus, Blake Williams*
16
+
17
+ * Experimental: Add `ViewComponent::Translatable`
18
+ * `t` and `translate` now will look first into the sidecar YAML translations file.
19
+ * `helpers.t` and `I18n.t` still reference the global Rails translation files.
20
+ * `l` and `localize` will still reference the global Rails translation files.
21
+
22
+ *Elia Schito*
23
+
24
+ * Fix rendering output of pass through slots when using HAML.
25
+
26
+ *Alex Robbin, Blake Williams*
27
+
28
+ * Experimental: call `._sidecar_files` to fetch the sidecar files for a given list of extensions, e.g. passing `["yml", "yaml"]`.
29
+
30
+ *Elia Schito*
31
+
32
+ * Fix bug where a single `jbuilder` template matched multiple template handlers.
33
+
34
+ *Niels Slot*
35
+
36
+ ## 2.28.0
37
+
38
+ * Include SlotableV2 by default in Base. **Note:** It's no longer necessary to include `ViewComponent::SlotableV2` to use Slots.
39
+
40
+ *Joel Hawksley*
41
+
42
+ * Prepend Preview routes instead of appending, accounting for cases where host application has catchall route.
43
+
44
+ *Joel Hawksley*
45
+
46
+ * Fix bug where blocks passed to lambda slots will render incorrectly in certain situations.
47
+
48
+ *Blake Williams*
49
+
50
+ ## 2.27.0
51
+
52
+ * Allow customization of the controller used in component tests.
53
+
54
+ *Alex Robbin*
55
+
56
+ * Generate preview at overridden path if one exists when using `--preview` flag.
57
+
58
+ *Nishiki Liu*
59
+
60
+ ## 2.26.1
61
+
62
+ * Fix bug that raises when trying to use a collection before the component has been compiled.
63
+
64
+ *Blake Williams*
65
+
5
66
  ## 2.26.0
6
67
 
7
68
  * Lazily evaluate component `content` in `render?`, preventing the `content` block from being evaluated when `render?` returns false.
data/README.md CHANGED
@@ -6,14 +6,6 @@ A framework for building reusable, testable & encapsulated view components in Ru
6
6
 
7
7
  See [viewcomponent.org](https://viewcomponent.org/) for documentation.
8
8
 
9
- ## Installation
10
-
11
- In `Gemfile`, add:
12
-
13
- ```ruby
14
- gem "view_component", require: "view_component/engine"
15
- ```
16
-
17
9
  ## Contributing
18
10
 
19
11
  This project is intended to be a safe, welcoming space for collaboration. Contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct. We recommend reading the [contributing guide](./CONTRIBUTING.md) as well.
@@ -8,7 +8,11 @@ module Preview
8
8
  check_class_collision suffix: "ComponentPreview"
9
9
 
10
10
  def create_preview_file
11
- template "component_preview.rb", File.join("test/components/previews", class_path, "#{file_name}_component_preview.rb")
11
+ preview_paths = Rails.application.config.view_component.preview_paths
12
+ return if preview_paths.count > 1
13
+
14
+ path_prefix = preview_paths.one? ? preview_paths.first : "test/components/previews"
15
+ template "component_preview.rb", File.join(path_prefix, class_path, "#{file_name}_component_preview.rb")
12
16
  end
13
17
 
14
18
  private
@@ -12,4 +12,5 @@ module ViewComponent
12
12
  autoload :TestHelpers
13
13
  autoload :TestCase
14
14
  autoload :TemplateError
15
+ autoload :Translatable
15
16
  end
@@ -12,6 +12,7 @@ module ViewComponent
12
12
  class Base < ActionView::Base
13
13
  include ActiveSupport::Configurable
14
14
  include ViewComponent::Previewable
15
+ include ViewComponent::SlotableV2
15
16
 
16
17
  ViewContextCalledBeforeRenderError = Class.new(StandardError)
17
18
 
@@ -130,7 +131,7 @@ module ViewComponent
130
131
  @helpers ||= controller.view_context
131
132
  end
132
133
 
133
- # Exposes .virutal_path as an instance method
134
+ # Exposes .virtual_path as an instance method
134
135
  def virtual_path
135
136
  self.class.virtual_path
136
137
  end
@@ -193,8 +194,9 @@ module ViewComponent
193
194
  end
194
195
 
195
196
  # The controller used for testing components.
196
- # Defaults to ApplicationController. This should be set early
197
- # in the initialization process and should be set to a string.
197
+ # Defaults to ApplicationController, but can be configured
198
+ # on a per-test basis using `with_controller_class`.
199
+ # This should be set early in the initialization process and should be a string.
198
200
  mattr_accessor :test_controller
199
201
  @@test_controller = "ApplicationController"
200
202
 
@@ -204,6 +206,47 @@ module ViewComponent
204
206
  class << self
205
207
  attr_accessor :source_location, :virtual_path
206
208
 
209
+ # EXPERIMENTAL: This API is experimental and may be removed at any time.
210
+ # Find sidecar files for the given extensions.
211
+ #
212
+ # The provided array of extensions is expected to contain
213
+ # strings starting without the "dot", example: `["erb", "haml"]`.
214
+ #
215
+ # For example, one might collect sidecar CSS files that need to be compiled.
216
+ def _sidecar_files(extensions)
217
+ return [] unless source_location
218
+
219
+ extensions = extensions.join(",")
220
+
221
+ # view files in a directory named like the component
222
+ directory = File.dirname(source_location)
223
+ filename = File.basename(source_location, ".rb")
224
+ component_name = name.demodulize.underscore
225
+
226
+ # Add support for nested components defined in the same file.
227
+ #
228
+ # e.g.
229
+ #
230
+ # class MyComponent < ViewComponent::Base
231
+ # class MyOtherComponent < ViewComponent::Base
232
+ # end
233
+ # end
234
+ #
235
+ # Without this, `MyOtherComponent` will not look for `my_component/my_other_component.html.erb`
236
+ nested_component_files = if name.include?("::") && component_name != filename
237
+ Dir["#{directory}/#{filename}/#{component_name}.*{#{extensions}}"]
238
+ else
239
+ []
240
+ end
241
+
242
+ # view files in the same directory as the component
243
+ sidecar_files = Dir["#{directory}/#{component_name}.*{#{extensions}}"]
244
+
245
+ sidecar_directory_files = Dir["#{directory}/#{component_name}/#{filename}.*{#{extensions}}"]
246
+
247
+ (sidecar_files - [source_location] + sidecar_directory_files + nested_component_files).uniq
248
+ end
249
+
207
250
  # Render a component collection.
208
251
  def with_collection(collection, **args)
209
252
  Collection.new(self, collection, **args)
@@ -266,6 +309,11 @@ module ViewComponent
266
309
  end
267
310
 
268
311
  def with_content_areas(*areas)
312
+ ActiveSupport::Deprecation.warn(
313
+ "`with_content_areas` is deprecated and will be removed in ViewComponent v3.0.0.\n" \
314
+ "Use slots (https://viewcomponent.org/guide/slots.html) instead."
315
+ )
316
+
269
317
  if areas.include?(:content)
270
318
  raise ArgumentError.new ":content is a reserved content area name. Please use another name, such as ':body'"
271
319
  end
@@ -324,6 +372,22 @@ module ViewComponent
324
372
  )
325
373
  end
326
374
 
375
+ def collection_parameter
376
+ if provided_collection_parameter
377
+ provided_collection_parameter
378
+ else
379
+ name && name.demodulize.underscore.chomp("_component").to_sym
380
+ end
381
+ end
382
+
383
+ def collection_counter_parameter
384
+ "#{collection_parameter}_counter".to_sym
385
+ end
386
+
387
+ def counter_argument_present?
388
+ instance_method(:initialize).parameters.map(&:second).include?(collection_counter_parameter)
389
+ end
390
+
327
391
  private
328
392
 
329
393
  def initialize_parameter_names
@@ -24,28 +24,6 @@ module ViewComponent
24
24
  )
25
25
  end
26
26
 
27
- # Remove any existing singleton methods,
28
- # as Ruby warns when redefining a method.
29
- component_class.remove_possible_singleton_method(:collection_parameter)
30
- component_class.remove_possible_singleton_method(:collection_counter_parameter)
31
- component_class.remove_possible_singleton_method(:counter_argument_present?)
32
-
33
- component_class.define_singleton_method(:collection_parameter) do
34
- if provided_collection_parameter
35
- provided_collection_parameter
36
- else
37
- name.demodulize.underscore.chomp("_component").to_sym
38
- end
39
- end
40
-
41
- component_class.define_singleton_method(:collection_counter_parameter) do
42
- "#{collection_parameter}_counter".to_sym
43
- end
44
-
45
- component_class.define_singleton_method(:counter_argument_present?) do
46
- instance_method(:initialize).parameters.map(&:second).include?(collection_counter_parameter)
47
- end
48
-
49
27
  if raise_errors
50
28
  component_class.validate_initialization_parameters!
51
29
  component_class.validate_collection_parameter!
@@ -83,7 +61,7 @@ module ViewComponent
83
61
  "elsif variant.to_sym == :#{variant}\n #{call_method_name(variant)}"
84
62
  end.join("\n")
85
63
 
86
- component_class.class_eval <<-RUBY
64
+ component_class.class_eval <<-RUBY, __FILE__, __LINE__ + 1
87
65
  def render_template_for(variant = nil)
88
66
  if variant.nil?
89
67
  call
@@ -136,50 +114,18 @@ module ViewComponent
136
114
  end
137
115
 
138
116
  def templates
139
- @templates ||= matching_views_in_source_location.each_with_object([]) do |path, memo|
140
- pieces = File.basename(path).split(".")
141
-
142
- memo << {
143
- path: path,
144
- variant: pieces.second.split("+").second&.to_sym,
145
- handler: pieces.last
146
- }
147
- end
148
- end
149
-
150
- def matching_views_in_source_location
151
- source_location = component_class.source_location
152
- return [] unless source_location
153
-
154
- extensions = ActionView::Template.template_handler_extensions.join(",")
155
-
156
- # view files in a directory named like the component
157
- directory = File.dirname(source_location)
158
- filename = File.basename(source_location, ".rb")
159
- component_name = component_class.name.demodulize.underscore
160
-
161
- # Add support for nested components defined in the same file.
162
- #
163
- # e.g.
164
- #
165
- # class MyComponent < ViewComponent::Base
166
- # class MyOtherComponent < ViewComponent::Base
167
- # end
168
- # end
169
- #
170
- # Without this, `MyOtherComponent` will not look for `my_component/my_other_component.html.erb`
171
- nested_component_files = if component_class.name.include?("::") && component_name != filename
172
- Dir["#{directory}/#{filename}/#{component_name}.*{#{extensions}}"]
173
- else
174
- []
117
+ @templates ||= begin
118
+ extensions = ActionView::Template.template_handler_extensions
119
+
120
+ component_class._sidecar_files(extensions).each_with_object([]) do |path, memo|
121
+ pieces = File.basename(path).split(".")
122
+ memo << {
123
+ path: path,
124
+ variant: pieces.second.split("+").second&.to_sym,
125
+ handler: pieces.last
126
+ }
127
+ end
175
128
  end
176
-
177
- # view files in the same directory as the component
178
- sidecar_files = Dir["#{directory}/#{component_name}.*{#{extensions}}"]
179
-
180
- sidecar_directory_files = Dir["#{directory}/#{component_name}/#{filename}.*{#{extensions}}"]
181
-
182
- (sidecar_files - [source_location] + sidecar_directory_files + nested_component_files)
183
129
  end
184
130
 
185
131
  def inline_calls
@@ -90,7 +90,7 @@ module ViewComponent
90
90
  options = app.config.view_component
91
91
 
92
92
  if options.show_previews
93
- app.routes.append do
93
+ app.routes.prepend do
94
94
  preview_controller = options.preview_controller.sub(/Controller$/, "").underscore
95
95
 
96
96
  get options.preview_route, to: "#{preview_controller}#index", as: :preview_view_components, internal: true
@@ -25,15 +25,22 @@ module ViewComponent
25
25
  return @content if defined?(@content)
26
26
 
27
27
  view_context = @parent.send(:view_context)
28
- @content = view_context.capture do
29
- if defined?(@_component_instance)
30
- # render_in is faster than `parent.render`
31
- @_component_instance.render_in(view_context, &@_content_block)
32
- elsif defined?(@_content)
33
- @_content
34
- elsif defined?(@_content_block)
35
- @_content_block.call
28
+
29
+ @content = if defined?(@_component_instance)
30
+ # render_in is faster than `parent.render`
31
+ if defined?(@_content_block)
32
+ view_context.capture do
33
+ @_component_instance.render_in(view_context, &@_content_block)
34
+ end
35
+ else
36
+ view_context.capture do
37
+ @_component_instance.render_in(view_context)
38
+ end
36
39
  end
40
+ elsif defined?(@_content)
41
+ @_content
42
+ elsif defined?(@_content_block)
43
+ view_context.capture(&@_content_block)
37
44
  end
38
45
 
39
46
  @content
@@ -49,14 +49,14 @@ module ViewComponent
49
49
 
50
50
  # If the slot is a collection, define an accesor that defaults to an empty array
51
51
  if collection
52
- class_eval <<-RUBY
52
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
53
53
  def #{accessor_name}
54
54
  content unless content_evaluated? # ensure content is loaded so slots will be defined
55
55
  #{instance_variable_name} ||= []
56
56
  end
57
57
  RUBY
58
58
  else
59
- class_eval <<-RUBY
59
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
60
60
  def #{accessor_name}
61
61
  content unless content_evaluated? # ensure content is loaded so slots will be defined
62
62
  #{instance_variable_name} if defined?(#{instance_variable_name})
@@ -226,7 +226,13 @@ module ViewComponent
226
226
  # Use `bind(self)` to ensure lambda is executed in the context of the
227
227
  # current component. This is necessary to allow the lambda to access helper
228
228
  # methods like `content_tag` as well as parent component state.
229
- renderable_value = slot_definition[:renderable_function].bind(self).call(*args, **kwargs, &block)
229
+ renderable_value = if block_given?
230
+ slot_definition[:renderable_function].bind(self).call(*args, **kwargs) do |*args, **kwargs|
231
+ view_context.capture(*args, **kwargs, &block)
232
+ end
233
+ else
234
+ slot_definition[:renderable_function].bind(self).call(*args, **kwargs)
235
+ end
230
236
 
231
237
  # Function calls can return components, so if it's a component handle it specially
232
238
  if renderable_value.respond_to?(:render_in)
@@ -35,7 +35,7 @@ module ViewComponent
35
35
  end
36
36
 
37
37
  def controller
38
- @controller ||= Base.test_controller.constantize.new.tap { |c| c.request = request }.extend(Rails.application.routes.url_helpers)
38
+ @controller ||= build_controller(Base.test_controller.constantize)
39
39
  end
40
40
 
41
41
  def request
@@ -47,7 +47,21 @@ module ViewComponent
47
47
 
48
48
  controller.view_context.lookup_context.variants = variant
49
49
  yield
50
+ ensure
50
51
  controller.view_context.lookup_context.variants = old_variants
51
52
  end
53
+
54
+ def with_controller_class(klass)
55
+ old_controller = defined?(@controller) && @controller
56
+
57
+ @controller = build_controller(klass)
58
+ yield
59
+ ensure
60
+ @controller = old_controller
61
+ end
62
+
63
+ def build_controller(klass)
64
+ klass.new.tap { |c| c.request = request }.extend(Rails.application.routes.url_helpers)
65
+ end
52
66
  end
53
67
  end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "set"
4
+ require "i18n"
5
+ require "action_view/helpers/translation_helper"
6
+ require "active_support/concern"
7
+
8
+ module ViewComponent
9
+ module Translatable
10
+ extend ActiveSupport::Concern
11
+
12
+ included do
13
+ class_attribute :i18n_backend, instance_writer: false, instance_predicate: false
14
+ end
15
+
16
+ class_methods do
17
+ def i18n_scope
18
+ @i18n_scope ||= virtual_path.sub(%r{^/}, "").gsub(%r{/_?}, ".")
19
+ end
20
+
21
+ def _after_compile
22
+ super
23
+
24
+ unless CompileCache.compiled? self
25
+ self.i18n_backend = I18nBackend.new(
26
+ i18n_scope: i18n_scope,
27
+ load_paths: _sidecar_files(%w[yml yaml]),
28
+ )
29
+ end
30
+ end
31
+ end
32
+
33
+ class I18nBackend < ::I18n::Backend::Simple
34
+ EMPTY_HASH = {}.freeze
35
+
36
+ def initialize(i18n_scope:, load_paths:)
37
+ @i18n_scope = i18n_scope.split(".")
38
+ @load_paths = load_paths
39
+ end
40
+
41
+ # Ensure the Simple backend won't load paths from ::I18n.load_path
42
+ def load_translations
43
+ super(@load_paths)
44
+ end
45
+
46
+ def scope_data(data)
47
+ @i18n_scope.reverse_each do |part|
48
+ data = { part => data}
49
+ end
50
+ data
51
+ end
52
+
53
+ def store_translations(locale, data, options = EMPTY_HASH)
54
+ super(locale, scope_data(data), options)
55
+ end
56
+ end
57
+
58
+ def translate(key = nil, locale: nil, **options)
59
+ locale ||= ::I18n.locale
60
+
61
+ key = "#{i18n_scope}#{key}" if key.start_with?(".")
62
+
63
+ result = catch(:exception) do
64
+ i18n_backend.translate(locale, key, options)
65
+ end
66
+
67
+ # Fallback to the global translations
68
+ if result.is_a? ::I18n::MissingTranslation
69
+ result = helpers.t(key, locale: locale, **options)
70
+ end
71
+
72
+ result
73
+ end
74
+ alias :t :translate
75
+
76
+ # Exposes .i18n_scope as an instance method
77
+ def i18n_scope
78
+ self.class.i18n_scope
79
+ end
80
+ end
81
+ end
@@ -3,9 +3,11 @@
3
3
  module ViewComponent
4
4
  module VERSION
5
5
  MAJOR = 2
6
- MINOR = 26
6
+ MINOR = 30
7
7
  PATCH = 0
8
8
 
9
9
  STRING = [MAJOR, MINOR, PATCH].join(".")
10
10
  end
11
11
  end
12
+
13
+ puts ViewComponent::VERSION::STRING if __FILE__ == $PROGRAM_NAME
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: 2.26.0
4
+ version: 2.30.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - GitHub Open Source
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-02-22 00:00:00.000000000 Z
11
+ date: 2021-04-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -128,6 +128,20 @@ dependencies:
128
128
  - - "~>"
129
129
  - !ruby/object:Gem::Version
130
130
  version: '1'
131
+ - !ruby/object:Gem::Dependency
132
+ name: jbuilder
133
+ requirement: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - "~>"
136
+ - !ruby/object:Gem::Version
137
+ version: '2'
138
+ type: :development
139
+ prerelease: false
140
+ version_requirements: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - "~>"
143
+ - !ruby/object:Gem::Version
144
+ version: '2'
131
145
  - !ruby/object:Gem::Dependency
132
146
  name: rubocop
133
147
  requirement: !ruby/object:Gem::Requirement
@@ -249,6 +263,7 @@ files:
249
263
  - lib/view_component/template_error.rb
250
264
  - lib/view_component/test_case.rb
251
265
  - lib/view_component/test_helpers.rb
266
+ - lib/view_component/translatable.rb
252
267
  - lib/view_component/version.rb
253
268
  homepage: https://github.com/github/view_component
254
269
  licenses:
@@ -270,7 +285,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
270
285
  - !ruby/object:Gem::Version
271
286
  version: '0'
272
287
  requirements: []
273
- rubygems_version: 3.0.3
288
+ rubygems_version: 3.1.2
274
289
  signing_key:
275
290
  specification_version: 4
276
291
  summary: View components for Rails