view_component 2.19.1 → 2.20.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.
Potentially problematic release.
This version of view_component might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/CHANGELOG.md +10 -0
- data/README.md +22 -18
- data/lib/rails/generators/test_unit/templates/component_test.rb.tt +1 -1
- data/lib/view_component.rb +1 -0
- data/lib/view_component/base.rb +6 -183
- data/lib/view_component/compiler.rb +214 -0
- data/lib/view_component/engine.rb +9 -4
- data/lib/view_component/preview.rb +4 -1
- data/lib/view_component/previewable.rb +10 -0
- data/lib/view_component/version.rb +2 -2
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: '09c638fb23b5832cf8ce411d1a07b50d2d56087858d8186ee2d4248c7e0576de'
|
4
|
+
data.tar.gz: b7ea4f439d382228a7c6911b2c0b959be52c7443cd4885a9d91159b55764d8b7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 43d6768b641f4fcb4f7a86488312400f6cb1922722f5654a98bebda771dbdc1a544e84d64b719880c822fec94621f1d402f6eaa805a21d383cab227b630b6503
|
7
|
+
data.tar.gz: 152a9f7ad4a6de0fa37df958d8a1877822bdf96f97ef81cbeebd9afb6224155e37725eb71338dd908061c0f0799e0cbf5dcf7c0c39d805ad5c471335649f4b97
|
data/CHANGELOG.md
CHANGED
@@ -2,6 +2,16 @@
|
|
2
2
|
|
3
3
|
## master
|
4
4
|
|
5
|
+
## 2.20.0
|
6
|
+
|
7
|
+
* Don't add `/test/components/previews` to preview_paths if directory doesn't exist.
|
8
|
+
|
9
|
+
*Andy Holland*
|
10
|
+
|
11
|
+
* Add `preview_controller` option to override the controller used for component previews.
|
12
|
+
|
13
|
+
*Matt Swanson, Blake Williams, Juan Manuel Ramallo*
|
14
|
+
|
5
15
|
## 2.19.1
|
6
16
|
|
7
17
|
* Check if `Rails.application` is loaded.
|
data/README.md
CHANGED
@@ -17,13 +17,7 @@ ViewComponent is tested for compatibility [with combinations of](https://github.
|
|
17
17
|
In `Gemfile`, add:
|
18
18
|
|
19
19
|
```ruby
|
20
|
-
gem "view_component"
|
21
|
-
```
|
22
|
-
|
23
|
-
In `config/application.rb`, add:
|
24
|
-
|
25
|
-
```bash
|
26
|
-
require "view_component/engine"
|
20
|
+
gem "view_component", require: "view_component/engine"
|
27
21
|
```
|
28
22
|
|
29
23
|
## Guide
|
@@ -197,7 +191,7 @@ To define a collection slot, add `collection: true`:
|
|
197
191
|
|
198
192
|
To define a slot with a custom Ruby class, pass `class_name`:
|
199
193
|
|
200
|
-
`with_slot :body, class_name: 'BodySlot`
|
194
|
+
`with_slot :body, class_name: 'BodySlot'`
|
201
195
|
|
202
196
|
_Note: Slot classes must be subclasses of `ViewComponent::Slot`._
|
203
197
|
|
@@ -627,7 +621,7 @@ Capybara matchers are available if the gem is installed:
|
|
627
621
|
require "view_component/test_case"
|
628
622
|
|
629
623
|
class MyComponentTest < ViewComponent::TestCase
|
630
|
-
|
624
|
+
def test_render_component
|
631
625
|
render_inline(TestComponent.new(title: "my title")) { "Hello, World!" }
|
632
626
|
|
633
627
|
assert_selector("span[title='my title']", text: "Hello, World!")
|
@@ -638,7 +632,7 @@ end
|
|
638
632
|
In the absence of `capybara`, assert against the return value of `render_inline`, which is an instance of `Nokogiri::HTML::DocumentFragment`:
|
639
633
|
|
640
634
|
```ruby
|
641
|
-
|
635
|
+
def test_render_component
|
642
636
|
result = render_inline(TestComponent.new(title: "my title")) { "Hello, World!" }
|
643
637
|
|
644
638
|
assert_includes result.css("span[title='my title']").to_html, "Hello, World!"
|
@@ -648,7 +642,7 @@ end
|
|
648
642
|
Alternatively, assert against the raw output of the component, which is exposed as `rendered_component`:
|
649
643
|
|
650
644
|
```ruby
|
651
|
-
|
645
|
+
def test_render_component
|
652
646
|
render_inline(TestComponent.new(title: "my title")) { "Hello, World!" }
|
653
647
|
|
654
648
|
assert_includes rendered_component, "Hello, World!"
|
@@ -658,7 +652,7 @@ end
|
|
658
652
|
To test components that use `with_content_areas`:
|
659
653
|
|
660
654
|
```ruby
|
661
|
-
|
655
|
+
def test_renders_content_areas_template_with_content
|
662
656
|
render_inline(ContentAreasComponent.new(footer: "Bye!")) do |component|
|
663
657
|
component.with(:title, "Hello!")
|
664
658
|
component.with(:body) { "Have a nice day." }
|
@@ -675,7 +669,7 @@ end
|
|
675
669
|
Use the `with_variant` helper to test specific variants:
|
676
670
|
|
677
671
|
```ruby
|
678
|
-
|
672
|
+
def test_render_component_for_tablet
|
679
673
|
with_variant :tablet do
|
680
674
|
render_inline(TestComponent.new(title: "my title")) { "Hello, tablets!" }
|
681
675
|
|
@@ -826,9 +820,19 @@ end
|
|
826
820
|
|
827
821
|
Which enables passing in a value with <http://localhost:3000/rails/components/cell_component/default?title=Custom+title&subtitle=Another+subtitle>.
|
828
822
|
|
823
|
+
#### Configuring preview controller
|
824
|
+
|
825
|
+
Previews can be extended to allow users to add authentication, authorization, before actions, or anything that the end user would need to meet their needs using the `preview_controller` option:
|
826
|
+
|
827
|
+
`config/application.rb`
|
828
|
+
|
829
|
+
```ruby
|
830
|
+
config.view_component.preview_controller = "MyPreviewController"
|
831
|
+
```
|
832
|
+
|
829
833
|
#### Configuring TestController
|
830
834
|
|
831
|
-
Component tests
|
835
|
+
Component tests assume the existence of an `ApplicationController` class, which be can be configured using the `test_controller` option:
|
832
836
|
|
833
837
|
`config/application.rb`
|
834
838
|
|
@@ -1115,10 +1119,10 @@ ViewComponent is built by:
|
|
1115
1119
|
|@johannesengl|@czj|@mrrooijen|@bradparker|@mattbrictson|
|
1116
1120
|
|Berlin, Germany|Paris, France|The Netherlands|Brisbane, Australia|San Francisco|
|
1117
1121
|
|
1118
|
-
|<img src="https://avatars.githubusercontent.com/mixergtz?s=256" alt="mixergtz" width="128" />|<img src="https://avatars.githubusercontent.com/jules2689?s=256" alt="jules2689" width="128" />|<img src="https://avatars.githubusercontent.com/g13ydson?s=256" alt="g13ydson" width="128" />|
|
1119
|
-
|
1120
|
-
|@mixergtz|@jules2689|@g13ydson|
|
1121
|
-
|Medellin, Colombia|Toronto, Canada|João Pessoa, Brazil|
|
1122
|
+
|<img src="https://avatars.githubusercontent.com/mixergtz?s=256" alt="mixergtz" width="128" />|<img src="https://avatars.githubusercontent.com/jules2689?s=256" alt="jules2689" width="128" />|<img src="https://avatars.githubusercontent.com/g13ydson?s=256" alt="g13ydson" width="128" />|<img src="https://avatars.githubusercontent.com/swanson?s=256" alt="swanson" width="128" />|
|
1123
|
+
|:---:|:---:|:---:|:---:|
|
1124
|
+
|@mixergtz|@jules2689|@g13ydson|@swanson|
|
1125
|
+
|Medellin, Colombia|Toronto, Canada|João Pessoa, Brazil|Indianapolis, IN|
|
1122
1126
|
|
1123
1127
|
## License
|
1124
1128
|
|
@@ -1,7 +1,7 @@
|
|
1
1
|
require "test_helper"
|
2
2
|
|
3
3
|
class <%= class_name %>ComponentTest < ViewComponent::TestCase
|
4
|
-
|
4
|
+
def test_component_renders_something_useful
|
5
5
|
# assert_equal(
|
6
6
|
# %(<span>Hello, components!</span>),
|
7
7
|
# render_inline(<%= class_name %>Component.new(message: "Hello, components!")).css("span").to_html
|
data/lib/view_component.rb
CHANGED
data/lib/view_component/base.rb
CHANGED
@@ -77,7 +77,7 @@ module ViewComponent
|
|
77
77
|
before_render
|
78
78
|
|
79
79
|
if render?
|
80
|
-
|
80
|
+
render_template_for(@variant)
|
81
81
|
else
|
82
82
|
""
|
83
83
|
end
|
@@ -208,16 +208,8 @@ module ViewComponent
|
|
208
208
|
super
|
209
209
|
end
|
210
210
|
|
211
|
-
def call_method_name(variant)
|
212
|
-
if variant.present? && variants.include?(variant)
|
213
|
-
"call_#{variant}"
|
214
|
-
else
|
215
|
-
"call"
|
216
|
-
end
|
217
|
-
end
|
218
|
-
|
219
211
|
def compiled?
|
220
|
-
|
212
|
+
template_compiler.compiled?
|
221
213
|
end
|
222
214
|
|
223
215
|
# Compile templates to instance methods, assuming they haven't been compiled already.
|
@@ -225,75 +217,11 @@ module ViewComponent
|
|
225
217
|
# Do as much work as possible in this step, as doing so reduces the amount
|
226
218
|
# of work done each time a component is rendered.
|
227
219
|
def compile(raise_errors: false)
|
228
|
-
|
229
|
-
|
230
|
-
if template_errors.present?
|
231
|
-
raise ViewComponent::TemplateError.new(template_errors) if raise_errors
|
232
|
-
return false
|
233
|
-
end
|
234
|
-
|
235
|
-
if instance_methods(false).include?(:before_render_check)
|
236
|
-
ActiveSupport::Deprecation.warn(
|
237
|
-
"`before_render_check` will be removed in v3.0.0. Use `before_render` instead."
|
238
|
-
)
|
239
|
-
end
|
240
|
-
|
241
|
-
# Remove any existing singleton methods,
|
242
|
-
# as Ruby warns when redefining a method.
|
243
|
-
remove_possible_singleton_method(:variants)
|
244
|
-
remove_possible_singleton_method(:collection_parameter)
|
245
|
-
remove_possible_singleton_method(:collection_counter_parameter)
|
246
|
-
remove_possible_singleton_method(:counter_argument_present?)
|
247
|
-
|
248
|
-
define_singleton_method(:variants) do
|
249
|
-
templates.map { |template| template[:variant] } + variants_from_inline_calls(inline_calls)
|
250
|
-
end
|
251
|
-
|
252
|
-
define_singleton_method(:collection_parameter) do
|
253
|
-
if provided_collection_parameter
|
254
|
-
provided_collection_parameter
|
255
|
-
else
|
256
|
-
name.demodulize.underscore.chomp("_component").to_sym
|
257
|
-
end
|
258
|
-
end
|
259
|
-
|
260
|
-
define_singleton_method(:collection_counter_parameter) do
|
261
|
-
"#{collection_parameter}_counter".to_sym
|
262
|
-
end
|
263
|
-
|
264
|
-
define_singleton_method(:counter_argument_present?) do
|
265
|
-
instance_method(:initialize).parameters.map(&:second).include?(collection_counter_parameter)
|
266
|
-
end
|
267
|
-
|
268
|
-
validate_collection_parameter! if raise_errors
|
269
|
-
|
270
|
-
# If template name annotations are turned on, a line is dynamically
|
271
|
-
# added with a comment. In this case, we want to return a different
|
272
|
-
# starting line number so errors that are raised will point to the
|
273
|
-
# correct line in the component template.
|
274
|
-
line_number =
|
275
|
-
if ActionView::Base.respond_to?(:annotate_rendered_view_with_filenames) &&
|
276
|
-
ActionView::Base.annotate_rendered_view_with_filenames
|
277
|
-
-2
|
278
|
-
else
|
279
|
-
-1
|
280
|
-
end
|
281
|
-
|
282
|
-
templates.each do |template|
|
283
|
-
# Remove existing compiled template methods,
|
284
|
-
# as Ruby warns when redefining a method.
|
285
|
-
method_name = call_method_name(template[:variant])
|
286
|
-
undef_method(method_name.to_sym) if instance_methods.include?(method_name.to_sym)
|
287
|
-
|
288
|
-
class_eval <<-RUBY, template[:path], line_number
|
289
|
-
def #{method_name}
|
290
|
-
@output_buffer = ActionView::OutputBuffer.new
|
291
|
-
#{compiled_template(template[:path])}
|
292
|
-
end
|
293
|
-
RUBY
|
294
|
-
end
|
220
|
+
template_compiler.compile(raise_errors: raise_errors)
|
221
|
+
end
|
295
222
|
|
296
|
-
|
223
|
+
def template_compiler
|
224
|
+
@_template_compiler ||= Compiler.new(self)
|
297
225
|
end
|
298
226
|
|
299
227
|
# we'll eventually want to update this to support other types
|
@@ -358,111 +286,6 @@ module ViewComponent
|
|
358
286
|
@provided_collection_parameter ||= nil
|
359
287
|
end
|
360
288
|
|
361
|
-
def compiled_template(file_path)
|
362
|
-
handler = ActionView::Template.handler_for_extension(File.extname(file_path).gsub(".", ""))
|
363
|
-
template = File.read(file_path)
|
364
|
-
|
365
|
-
if handler.method(:call).parameters.length > 1
|
366
|
-
handler.call(self, template)
|
367
|
-
else
|
368
|
-
handler.call(OpenStruct.new(source: template, identifier: identifier, type: type))
|
369
|
-
end
|
370
|
-
end
|
371
|
-
|
372
|
-
def inline_calls
|
373
|
-
@inline_calls ||=
|
374
|
-
begin
|
375
|
-
# Fetch only ViewComponent ancestor classes to limit the scope of
|
376
|
-
# finding inline calls
|
377
|
-
view_component_ancestors =
|
378
|
-
ancestors.take_while { |ancestor| ancestor != ViewComponent::Base } - included_modules
|
379
|
-
|
380
|
-
view_component_ancestors.flat_map { |ancestor| ancestor.instance_methods(false).grep(/^call/) }.uniq
|
381
|
-
end
|
382
|
-
end
|
383
|
-
|
384
|
-
def inline_calls_defined_on_self
|
385
|
-
@inline_calls_defined_on_self ||= instance_methods(false).grep(/^call/)
|
386
|
-
end
|
387
|
-
|
388
|
-
def matching_views_in_source_location
|
389
|
-
return [] unless source_location
|
390
|
-
|
391
|
-
location_without_extension = source_location.chomp(File.extname(source_location))
|
392
|
-
|
393
|
-
extensions = ActionView::Template.template_handler_extensions.join(",")
|
394
|
-
|
395
|
-
# view files in the same directory as the component
|
396
|
-
sidecar_files = Dir["#{location_without_extension}.*{#{extensions}}"]
|
397
|
-
|
398
|
-
# view files in a directory named like the component
|
399
|
-
directory = File.dirname(source_location)
|
400
|
-
filename = File.basename(source_location, ".rb")
|
401
|
-
component_name = name.demodulize.underscore
|
402
|
-
|
403
|
-
sidecar_directory_files = Dir["#{directory}/#{component_name}/#{filename}.*{#{extensions}}"]
|
404
|
-
|
405
|
-
(sidecar_files - [source_location] + sidecar_directory_files)
|
406
|
-
end
|
407
|
-
|
408
|
-
def templates
|
409
|
-
@templates ||=
|
410
|
-
matching_views_in_source_location.each_with_object([]) do |path, memo|
|
411
|
-
pieces = File.basename(path).split(".")
|
412
|
-
|
413
|
-
memo << {
|
414
|
-
path: path,
|
415
|
-
variant: pieces.second.split("+").second&.to_sym,
|
416
|
-
handler: pieces.last
|
417
|
-
}
|
418
|
-
end
|
419
|
-
end
|
420
|
-
|
421
|
-
def template_errors
|
422
|
-
@template_errors ||=
|
423
|
-
begin
|
424
|
-
errors = []
|
425
|
-
|
426
|
-
if (templates + inline_calls).empty?
|
427
|
-
errors << "Could not find a template file or inline render method for #{self}."
|
428
|
-
end
|
429
|
-
|
430
|
-
if templates.count { |template| template[:variant].nil? } > 1
|
431
|
-
errors << "More than one template found for #{self}. There can only be one default template file per component."
|
432
|
-
end
|
433
|
-
|
434
|
-
invalid_variants = templates
|
435
|
-
.group_by { |template| template[:variant] }
|
436
|
-
.map { |variant, grouped| variant if grouped.length > 1 }
|
437
|
-
.compact
|
438
|
-
.sort
|
439
|
-
|
440
|
-
unless invalid_variants.empty?
|
441
|
-
errors << "More than one template found for #{'variant'.pluralize(invalid_variants.count)} #{invalid_variants.map { |v| "'#{v}'" }.to_sentence} in #{self}. There can only be one template file per variant."
|
442
|
-
end
|
443
|
-
|
444
|
-
if templates.find { |template| template[:variant].nil? } && inline_calls_defined_on_self.include?(:call)
|
445
|
-
errors << "Template file and inline render method found for #{self}. There can only be a template file or inline render method per component."
|
446
|
-
end
|
447
|
-
|
448
|
-
duplicate_template_file_and_inline_variant_calls =
|
449
|
-
templates.pluck(:variant) & variants_from_inline_calls(inline_calls_defined_on_self)
|
450
|
-
|
451
|
-
unless duplicate_template_file_and_inline_variant_calls.empty?
|
452
|
-
count = duplicate_template_file_and_inline_variant_calls.count
|
453
|
-
|
454
|
-
errors << "Template #{'file'.pluralize(count)} and inline render #{'method'.pluralize(count)} found for #{'variant'.pluralize(count)} #{duplicate_template_file_and_inline_variant_calls.map { |v| "'#{v}'" }.to_sentence} in #{self}. There can only be a template file or inline render method per variant."
|
455
|
-
end
|
456
|
-
|
457
|
-
errors
|
458
|
-
end
|
459
|
-
end
|
460
|
-
|
461
|
-
def variants_from_inline_calls(calls)
|
462
|
-
calls.reject { |call| call == :call }.map do |variant_call|
|
463
|
-
variant_call.to_s.sub("call_", "").to_sym
|
464
|
-
end
|
465
|
-
end
|
466
289
|
end
|
467
290
|
|
468
291
|
ActiveSupport.run_load_hooks(:view_component, self)
|
@@ -0,0 +1,214 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ViewComponent
|
4
|
+
class Compiler
|
5
|
+
def initialize(component_class)
|
6
|
+
@component_class = component_class
|
7
|
+
end
|
8
|
+
|
9
|
+
def compiled?
|
10
|
+
CompileCache.compiled?(component_class)
|
11
|
+
end
|
12
|
+
|
13
|
+
def compile(raise_errors: false)
|
14
|
+
return if compiled?
|
15
|
+
|
16
|
+
if template_errors.present?
|
17
|
+
raise ViewComponent::TemplateError.new(template_errors) if raise_errors
|
18
|
+
return false
|
19
|
+
end
|
20
|
+
|
21
|
+
if component_class.instance_methods(false).include?(:before_render_check)
|
22
|
+
ActiveSupport::Deprecation.warn(
|
23
|
+
"`before_render_check` will be removed in v3.0.0. Use `before_render` instead."
|
24
|
+
)
|
25
|
+
end
|
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
|
+
component_class.validate_collection_parameter! if raise_errors
|
50
|
+
|
51
|
+
templates.each do |template|
|
52
|
+
# Remove existing compiled template methods,
|
53
|
+
# as Ruby warns when redefining a method.
|
54
|
+
method_name = call_method_name(template[:variant])
|
55
|
+
component_class.send(:undef_method, method_name.to_sym) if component_class.instance_methods.include?(method_name.to_sym)
|
56
|
+
|
57
|
+
component_class.class_eval <<-RUBY, template[:path], -1
|
58
|
+
def #{method_name}
|
59
|
+
@output_buffer = ActionView::OutputBuffer.new
|
60
|
+
#{compiled_template(template[:path])}
|
61
|
+
end
|
62
|
+
RUBY
|
63
|
+
end
|
64
|
+
|
65
|
+
define_render_template_for
|
66
|
+
|
67
|
+
CompileCache.register(component_class)
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
attr_reader :component_class
|
73
|
+
|
74
|
+
def define_render_template_for
|
75
|
+
component_class.send(:undef_method, :render_template_for) if component_class.instance_methods.include?(:render_template_for)
|
76
|
+
|
77
|
+
variant_elsifs = variants.compact.uniq.map do |variant|
|
78
|
+
"elsif variant.to_sym == :#{variant}\n #{call_method_name(variant)}"
|
79
|
+
end.join("\n")
|
80
|
+
|
81
|
+
component_class.class_eval <<-RUBY
|
82
|
+
def render_template_for(variant = nil)
|
83
|
+
if variant.nil?
|
84
|
+
call
|
85
|
+
#{variant_elsifs}
|
86
|
+
else
|
87
|
+
call
|
88
|
+
end
|
89
|
+
end
|
90
|
+
RUBY
|
91
|
+
|
92
|
+
end
|
93
|
+
|
94
|
+
def template_errors
|
95
|
+
@_template_errors ||= begin
|
96
|
+
errors = []
|
97
|
+
|
98
|
+
if (templates + inline_calls).empty?
|
99
|
+
errors << "Could not find a template file or inline render method for #{component_class}."
|
100
|
+
end
|
101
|
+
|
102
|
+
if templates.count { |template| template[:variant].nil? } > 1
|
103
|
+
errors << "More than one template found for #{component_class}. There can only be one default template file per component."
|
104
|
+
end
|
105
|
+
|
106
|
+
invalid_variants = templates
|
107
|
+
.group_by { |template| template[:variant] }
|
108
|
+
.map { |variant, grouped| variant if grouped.length > 1 }
|
109
|
+
.compact
|
110
|
+
.sort
|
111
|
+
|
112
|
+
unless invalid_variants.empty?
|
113
|
+
errors << "More than one template found for #{'variant'.pluralize(invalid_variants.count)} #{invalid_variants.map { |v| "'#{v}'" }.to_sentence} in #{component_class}. There can only be one template file per variant."
|
114
|
+
end
|
115
|
+
|
116
|
+
if templates.find { |template| template[:variant].nil? } && inline_calls_defined_on_self.include?(:call)
|
117
|
+
errors << "Template file and inline render method found for #{component_class}. There can only be a template file or inline render method per component."
|
118
|
+
end
|
119
|
+
|
120
|
+
duplicate_template_file_and_inline_variant_calls =
|
121
|
+
templates.pluck(:variant) & variants_from_inline_calls(inline_calls_defined_on_self)
|
122
|
+
|
123
|
+
unless duplicate_template_file_and_inline_variant_calls.empty?
|
124
|
+
count = duplicate_template_file_and_inline_variant_calls.count
|
125
|
+
|
126
|
+
errors << "Template #{'file'.pluralize(count)} and inline render #{'method'.pluralize(count)} found for #{'variant'.pluralize(count)} #{duplicate_template_file_and_inline_variant_calls.map { |v| "'#{v}'" }.to_sentence} in #{component_class}. There can only be a template file or inline render method per variant."
|
127
|
+
end
|
128
|
+
|
129
|
+
errors
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def templates
|
134
|
+
@templates ||= matching_views_in_source_location.each_with_object([]) do |path, memo|
|
135
|
+
pieces = File.basename(path).split(".")
|
136
|
+
|
137
|
+
memo << {
|
138
|
+
path: path,
|
139
|
+
variant: pieces.second.split("+").second&.to_sym,
|
140
|
+
handler: pieces.last
|
141
|
+
}
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def matching_views_in_source_location
|
146
|
+
source_location = component_class.source_location
|
147
|
+
return [] unless source_location
|
148
|
+
|
149
|
+
location_without_extension = source_location.chomp(File.extname(source_location))
|
150
|
+
|
151
|
+
extensions = ActionView::Template.template_handler_extensions.join(",")
|
152
|
+
|
153
|
+
# view files in the same directory as the component
|
154
|
+
sidecar_files = Dir["#{location_without_extension}.*{#{extensions}}"]
|
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
|
+
sidecar_directory_files = Dir["#{directory}/#{component_name}/#{filename}.*{#{extensions}}"]
|
162
|
+
|
163
|
+
(sidecar_files - [source_location] + sidecar_directory_files)
|
164
|
+
end
|
165
|
+
|
166
|
+
def inline_calls
|
167
|
+
@inline_calls ||= begin
|
168
|
+
# Fetch only ViewComponent ancestor classes to limit the scope of
|
169
|
+
# finding inline calls
|
170
|
+
view_component_ancestors =
|
171
|
+
component_class.ancestors.take_while { |ancestor| ancestor != ViewComponent::Base } - component_class.included_modules
|
172
|
+
|
173
|
+
view_component_ancestors.flat_map { |ancestor| ancestor.instance_methods(false).grep(/^call/) }.uniq
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
def inline_calls_defined_on_self
|
178
|
+
@inline_calls_defined_on_self ||= component_class.instance_methods(false).grep(/^call/)
|
179
|
+
end
|
180
|
+
|
181
|
+
def variants
|
182
|
+
@_variants = (
|
183
|
+
templates.map { |template| template[:variant] } + variants_from_inline_calls(inline_calls)
|
184
|
+
).compact.uniq
|
185
|
+
end
|
186
|
+
|
187
|
+
def variants_from_inline_calls(calls)
|
188
|
+
calls.reject { |call| call == :call }.map do |variant_call|
|
189
|
+
variant_call.to_s.sub("call_", "").to_sym
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
# :nocov:
|
194
|
+
def compiled_template(file_path)
|
195
|
+
handler = ActionView::Template.handler_for_extension(File.extname(file_path).gsub(".", ""))
|
196
|
+
template = File.read(file_path)
|
197
|
+
|
198
|
+
if handler.method(:call).parameters.length > 1
|
199
|
+
handler.call(component_class, template)
|
200
|
+
else
|
201
|
+
handler.call(OpenStruct.new(source: template, identifier: component_class.identifier, type: component_class.type))
|
202
|
+
end
|
203
|
+
end
|
204
|
+
# :nocov:
|
205
|
+
|
206
|
+
def call_method_name(variant)
|
207
|
+
if variant.present? && variants.include?(variant)
|
208
|
+
"call_#{variant}"
|
209
|
+
else
|
210
|
+
"call"
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|
@@ -14,9 +14,12 @@ module ViewComponent
|
|
14
14
|
options.render_monkey_patch_enabled = true if options.render_monkey_patch_enabled.nil?
|
15
15
|
options.show_previews = Rails.env.development? if options.show_previews.nil?
|
16
16
|
options.preview_route ||= ViewComponent::Base.preview_route
|
17
|
+
options.preview_controller ||= ViewComponent::Base.preview_controller
|
17
18
|
|
18
19
|
if options.show_previews
|
19
|
-
options.preview_paths << "#{Rails.root}/test/components/previews" if defined?(Rails.root)
|
20
|
+
options.preview_paths << "#{Rails.root}/test/components/previews" if defined?(Rails.root) && Dir.exist?(
|
21
|
+
"#{Rails.root}/test/components/previews"
|
22
|
+
)
|
20
23
|
|
21
24
|
if options.preview_path.present?
|
22
25
|
ActiveSupport::Deprecation.warn(
|
@@ -87,9 +90,11 @@ module ViewComponent
|
|
87
90
|
options = app.config.view_component
|
88
91
|
|
89
92
|
if options.show_previews
|
90
|
-
app.routes.
|
91
|
-
|
92
|
-
|
93
|
+
app.routes.append do
|
94
|
+
preview_controller = options.preview_controller.sub(/Controller$/, "").underscore
|
95
|
+
|
96
|
+
get options.preview_route, to: "#{preview_controller}#index", as: :preview_view_components, internal: true
|
97
|
+
get "#{options.preview_route}/*path", to: "#{preview_controller}#previews", as: :preview_view_component, internal: true
|
93
98
|
end
|
94
99
|
end
|
95
100
|
|
@@ -80,7 +80,10 @@ module ViewComponent # :nodoc:
|
|
80
80
|
end
|
81
81
|
|
82
82
|
path = Dir["#{preview_path}/#{preview_name}_preview/#{example}.html.*"].first
|
83
|
-
Pathname.new(path)
|
83
|
+
Pathname.new(path)
|
84
|
+
.relative_path_from(Pathname.new(preview_path))
|
85
|
+
.to_s
|
86
|
+
.sub(/\..*$/, "")
|
84
87
|
end
|
85
88
|
|
86
89
|
private
|
@@ -41,6 +41,16 @@ module ViewComponent # :nodoc:
|
|
41
41
|
mattr_accessor :preview_route, instance_writer: false do
|
42
42
|
"/rails/view_components"
|
43
43
|
end
|
44
|
+
|
45
|
+
# Set the controller to be used for previewing components through app configuration:
|
46
|
+
#
|
47
|
+
# config.view_component.preview_controller = "MyPreviewController"
|
48
|
+
#
|
49
|
+
# Defaults to the provided +ViewComponentsController+
|
50
|
+
#
|
51
|
+
mattr_accessor :preview_controller, instance_writer: false do
|
52
|
+
"ViewComponentsController"
|
53
|
+
end
|
44
54
|
end
|
45
55
|
end
|
46
56
|
end
|
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.
|
4
|
+
version: 2.20.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: 2020-
|
11
|
+
date: 2020-10-19 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -215,6 +215,7 @@ files:
|
|
215
215
|
- lib/view_component/base.rb
|
216
216
|
- lib/view_component/collection.rb
|
217
217
|
- lib/view_component/compile_cache.rb
|
218
|
+
- lib/view_component/compiler.rb
|
218
219
|
- lib/view_component/engine.rb
|
219
220
|
- lib/view_component/preview.rb
|
220
221
|
- lib/view_component/preview_template_error.rb
|