actionview-component 1.13.0 → 1.14.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +6 -0
- data/Gemfile.lock +1 -1
- data/README.md +43 -55
- data/actionview-component.gemspec +2 -2
- data/app/controllers/rails/components_controller.rb +4 -4
- data/lib/action_view/component.rb +1 -21
- data/lib/action_view/component/base.rb +1 -262
- data/lib/action_view/component/preview.rb +1 -72
- data/lib/action_view/component/railtie.rb +1 -1
- data/lib/action_view/component/test_case.rb +1 -4
- data/lib/rails/generators/component/component_generator.rb +1 -1
- data/lib/rails/generators/test_unit/templates/component_test.rb.tt +1 -1
- data/lib/view_component.rb +29 -0
- data/lib/view_component/base.rb +273 -0
- data/lib/view_component/conversion.rb +9 -0
- data/lib/view_component/engine.rb +65 -0
- data/lib/view_component/preview.rb +77 -0
- data/lib/view_component/previewable.rb +25 -0
- data/lib/view_component/render_monkey_patch.rb +31 -0
- data/lib/view_component/rendering_monkey_patch.rb +13 -0
- data/lib/view_component/template_error.rb +9 -0
- data/lib/view_component/test_case.rb +9 -0
- data/lib/view_component/test_helpers.rb +43 -0
- data/lib/view_component/version.rb +11 -0
- data/script/console +1 -1
- metadata +14 -10
- data/lib/action_view/component/conversion.rb +0 -11
- data/lib/action_view/component/engine.rb +0 -67
- data/lib/action_view/component/previewable.rb +0 -27
- data/lib/action_view/component/render_monkey_patch.rb +0 -34
- data/lib/action_view/component/rendering_monkey_patch.rb +0 -15
- data/lib/action_view/component/template_error.rb +0 -11
- data/lib/action_view/component/test_helpers.rb +0 -45
- data/lib/action_view/component/version.rb +0 -13
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 20f4f3d429b7cd0741316064cdfd5318a4ccd17f97dbaa00853a2976fc1b8901
|
4
|
+
data.tar.gz: '086d91730bd9cfa7f75eb3afff4f97d142a1f968b8e5d7d769024c5599e1c22b'
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a6c458998089d44a6b3f0daa920c5c3b5e19d73d8e7e7fa13b96c1cc0df8abf9f27a61ea1c43bd840eff4aec0327fad1109cd15996ca84304698d772754d0d85
|
7
|
+
data.tar.gz: 578b279ca15f8d436676a1f579bfa2ffb32d7cf97a0302668bc9142c89256c4124a653256c6daa9cb0d3a2b368166116e0b0b6427e854e06ca193555ab149bb4
|
data/CHANGELOG.md
CHANGED
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -1,11 +1,25 @@
|
|
1
|
-
|
1
|
+
# ViewComponent
|
2
|
+
A view component framework for Rails.
|
2
3
|
|
3
|
-
|
4
|
+
**Current Status**: Used in production at GitHub. Because of this, all changes will be thoroughly vetted, which could slow down the process of contributing. We will do our best to actively communicate status of pull requests with any contributors. If you have any substantial changes that you would like to make, it would be great to first [open an issue](http://github.com/github/actionview-component/issues/new) to discuss them with us.
|
4
5
|
|
5
|
-
|
6
|
-
`ActionView::Component` is a framework for building view components in Rails.
|
6
|
+
## Migration in progress
|
7
7
|
|
8
|
-
|
8
|
+
This gem is in the process of a name / API change from `ActionView::Component` to `ViewComponent`, see https://github.com/github/actionview-component/issues/206.
|
9
|
+
|
10
|
+
### What's changing in the migration
|
11
|
+
|
12
|
+
1. `ActionView::Component::Base` is now `ViewComponent::Base`.
|
13
|
+
1. Components can only be rendered with `render(MyComponent.new)` syntax.
|
14
|
+
1. Validations are no longer supported by default.
|
15
|
+
|
16
|
+
### How to migrate to ViewComponent
|
17
|
+
|
18
|
+
1. In `application.rb`, require `view_component/engine`
|
19
|
+
1. Update components to inherit from `ViewComponent::Base`.
|
20
|
+
1. Update component tests to inherit from `ViewComponent::TestCase`.
|
21
|
+
1. Update component previews to inherit from `ViewComponent::Preview`.
|
22
|
+
1. Include `ViewComponent::TestHelpers` in your test suite.
|
9
23
|
|
10
24
|
## Roadmap
|
11
25
|
|
@@ -36,14 +50,14 @@ $ bundle
|
|
36
50
|
In `config/application.rb`, add:
|
37
51
|
|
38
52
|
```bash
|
39
|
-
require "
|
53
|
+
require "view_component/engine"
|
40
54
|
```
|
41
55
|
|
42
56
|
## Guide
|
43
57
|
|
44
58
|
### What are components?
|
45
59
|
|
46
|
-
`
|
60
|
+
`ViewComponent`s are Ruby classes that are used to render views. They take data as input and return output-safe HTML. Think of them as an evolution of the presenter/decorator/view model pattern, inspired by [React Components](https://reactjs.org/docs/react-component.html).
|
47
61
|
|
48
62
|
### Why components?
|
49
63
|
|
@@ -69,11 +83,11 @@ Our views often fail even the most basic standards of code quality we expect out
|
|
69
83
|
|
70
84
|
#### Testing
|
71
85
|
|
72
|
-
`
|
86
|
+
`ViewComponent` allows views to be unit-tested. In the main GitHub codebase, our unit tests run in around 25ms/test, vs. ~6s/test for integration tests.
|
73
87
|
|
74
88
|
#### Code Coverage
|
75
89
|
|
76
|
-
`
|
90
|
+
`ViewComponent` is at least partially compatible with code coverage tools. We’ve seen some success with SimpleCov.
|
77
91
|
|
78
92
|
#### Data flow
|
79
93
|
|
@@ -87,19 +101,17 @@ Components are most effective in cases where view code is reused or needs to be
|
|
87
101
|
|
88
102
|
#### Conventions
|
89
103
|
|
90
|
-
Components are subclasses of `
|
104
|
+
Components are subclasses of `ViewComponent::Base` and live in `app/components`. You may wish to create an `ApplicationComponent` that is a subclass of `ViewComponent::Base` and inherit from that instead.
|
91
105
|
|
92
106
|
Component class names end in -`Component`.
|
93
107
|
|
94
108
|
Component module names are plural, as they are for controllers. (`Users::AvatarComponent`)
|
95
109
|
|
96
|
-
|
97
|
-
|
98
|
-
Content passed to an `ActionView::Component` as a block is captured and assigned to the `content` accessor.
|
110
|
+
Content passed to a `ViewComponent` as a block is captured and assigned to the `content` accessor.
|
99
111
|
|
100
112
|
#### Quick start
|
101
113
|
|
102
|
-
Use the component generator to create a new `
|
114
|
+
Use the component generator to create a new `ViewComponent`.
|
103
115
|
|
104
116
|
The generator accepts the component name and the list of accepted properties as arguments:
|
105
117
|
|
@@ -111,7 +123,7 @@ bin/rails generate component Example title content
|
|
111
123
|
create app/components/example_component.html.erb
|
112
124
|
```
|
113
125
|
|
114
|
-
`
|
126
|
+
`ViewComponent` includes template generators for the `erb`, `haml`, and `slim` template engines and will use the template engine specified in your Rails config (`config.generators.template_engine`) by default.
|
115
127
|
|
116
128
|
If you want to override this behavior, you can pass the template engine as an option to the generator:
|
117
129
|
|
@@ -125,13 +137,11 @@ bin/rails generate component Example title content --template-engine slim
|
|
125
137
|
|
126
138
|
#### Implementation
|
127
139
|
|
128
|
-
|
140
|
+
A `ViewComponent` is a Ruby file and corresponding template file (in any format supported by Rails) with the same base name:
|
129
141
|
|
130
142
|
`app/components/test_component.rb`:
|
131
143
|
```ruby
|
132
|
-
class TestComponent <
|
133
|
-
validates :content, :title, presence: true
|
134
|
-
|
144
|
+
class TestComponent < ViewComponent::Base
|
135
145
|
def initialize(title:)
|
136
146
|
@title = title
|
137
147
|
end
|
@@ -161,29 +171,13 @@ Which returns:
|
|
161
171
|
<span title="my title">Hello, World!</span>
|
162
172
|
```
|
163
173
|
|
164
|
-
#### Error case
|
165
|
-
|
166
|
-
If the component is rendered with a blank title:
|
167
|
-
|
168
|
-
```erb
|
169
|
-
<%= render(TestComponent.new(title: "")) do %>
|
170
|
-
Hello, World!
|
171
|
-
<% end %>
|
172
|
-
```
|
173
|
-
|
174
|
-
An error will be raised:
|
175
|
-
|
176
|
-
`ActiveModel::ValidationError: Validation failed: Title can't be blank`
|
177
|
-
|
178
174
|
#### Content Areas
|
179
175
|
|
180
176
|
A component can declare additional content areas to be rendered in the component. For example:
|
181
177
|
|
182
178
|
`app/components/modal_component.rb`:
|
183
179
|
```ruby
|
184
|
-
class ModalComponent <
|
185
|
-
validates :user, :header, :body, presence: true
|
186
|
-
|
180
|
+
class ModalComponent < ViewComponent::Base
|
187
181
|
with_content_areas :header, :body
|
188
182
|
|
189
183
|
def initialize(user:)
|
@@ -231,9 +225,7 @@ This allows a few different combinations of ways to render the component:
|
|
231
225
|
|
232
226
|
`app/components/modal_component.rb`:
|
233
227
|
```ruby
|
234
|
-
class ModalComponent <
|
235
|
-
validates :header, :body, presence: true
|
236
|
-
|
228
|
+
class ModalComponent < ViewComponent::Base
|
237
229
|
with_content_areas :header, :body
|
238
230
|
|
239
231
|
def initialize(header:)
|
@@ -257,9 +249,7 @@ end
|
|
257
249
|
|
258
250
|
`app/components/modal_component.rb`:
|
259
251
|
```ruby
|
260
|
-
class ModalComponent <
|
261
|
-
validates :header, :body, presence: true
|
262
|
-
|
252
|
+
class ModalComponent < ViewComponent::Base
|
263
253
|
with_content_areas :header, :body
|
264
254
|
|
265
255
|
def initialize(header: nil)
|
@@ -293,9 +283,7 @@ end
|
|
293
283
|
|
294
284
|
`app/components/modal_component.rb`:
|
295
285
|
```ruby
|
296
|
-
class ModalComponent <
|
297
|
-
validates :body, presence: true
|
298
|
-
|
286
|
+
class ModalComponent < ViewComponent::Base
|
299
287
|
with_content_areas :header, :body
|
300
288
|
|
301
289
|
def initialize(header: nil)
|
@@ -372,7 +360,7 @@ The `#render?` hook allows you to move this logic into the Ruby class, leaving y
|
|
372
360
|
|
373
361
|
```ruby
|
374
362
|
# app/components/confirm_email_component.rb
|
375
|
-
class ConfirmEmailComponent <
|
363
|
+
class ConfirmEmailComponent < ViewComponent::Base
|
376
364
|
def initialize(user:)
|
377
365
|
@user = user
|
378
366
|
end
|
@@ -402,9 +390,9 @@ end
|
|
402
390
|
Components are unit tested directly. The `render_inline` test helper is compatible with Capybara matchers, allowing us to test the component above as:
|
403
391
|
|
404
392
|
```ruby
|
405
|
-
require "
|
393
|
+
require "view_component/test_case"
|
406
394
|
|
407
|
-
class MyComponentTest <
|
395
|
+
class MyComponentTest < ViewComponent::TestCase
|
408
396
|
test "render component" do
|
409
397
|
render_inline(TestComponent.new(title: "my title")) { "Hello, World!" }
|
410
398
|
|
@@ -430,7 +418,7 @@ end
|
|
430
418
|
```
|
431
419
|
|
432
420
|
### Previewing Components
|
433
|
-
`
|
421
|
+
`ViewComponent::Preview` provides a way to see how components look by visiting a special URL that renders them.
|
434
422
|
In the previous example, the preview class for `TestComponent` would be called `TestComponentPreview` and located in `test/components/previews/test_component_preview.rb`.
|
435
423
|
To see the preview of the component with a given title, implement a method that renders the component.
|
436
424
|
You can define as many examples as you want:
|
@@ -438,7 +426,7 @@ You can define as many examples as you want:
|
|
438
426
|
```ruby
|
439
427
|
# test/components/previews/test_component_preview.rb
|
440
428
|
|
441
|
-
class TestComponentPreview <
|
429
|
+
class TestComponentPreview < ViewComponent::Preview
|
442
430
|
def with_default_title
|
443
431
|
render(TestComponent.new(title: "Test component default"))
|
444
432
|
end
|
@@ -457,7 +445,7 @@ Previews use the application layout by default, but you can also use other layou
|
|
457
445
|
```ruby
|
458
446
|
# test/components/previews/test_component_preview.rb
|
459
447
|
|
460
|
-
class TestComponentPreview <
|
448
|
+
class TestComponentPreview < ViewComponent::Preview
|
461
449
|
layout "admin"
|
462
450
|
|
463
451
|
...
|
@@ -488,13 +476,13 @@ If you're using RSpec, you can configure component specs to have access to test
|
|
488
476
|
`spec/rails_helper.rb`:
|
489
477
|
|
490
478
|
```ruby
|
491
|
-
require "
|
479
|
+
require "view_component/test_helpers"
|
492
480
|
|
493
481
|
RSpec.configure do |config|
|
494
482
|
# ...
|
495
483
|
|
496
484
|
# Ensure that the test helpers are available in component specs
|
497
|
-
config.include
|
485
|
+
config.include ViewComponent::TestHelpers, type: :component
|
498
486
|
end
|
499
487
|
```
|
500
488
|
|
@@ -508,7 +496,7 @@ config.action_view_component.preview_path = "#{Rails.root}/spec/components/previ
|
|
508
496
|
|
509
497
|
### Initializer requirement
|
510
498
|
|
511
|
-
|
499
|
+
`ViewComponent` requires the presence of an `initialize` method in each component.
|
512
500
|
|
513
501
|
## Frequently Asked Questions
|
514
502
|
|
@@ -522,7 +510,7 @@ Inline templates have been removed (for now) due to concerns raised by [@soutaro
|
|
522
510
|
|
523
511
|
### Isn't this just like X library?
|
524
512
|
|
525
|
-
`
|
513
|
+
`ViewComponent` is far from a novel idea! Popular implementations of view components in Ruby include, but are not limited to:
|
526
514
|
|
527
515
|
- [trailblazer/cells](https://github.com/trailblazer/cells)
|
528
516
|
- [dry-rb/dry-view](https://github.com/dry-rb/dry-view)
|
@@ -3,11 +3,11 @@
|
|
3
3
|
|
4
4
|
lib = File.expand_path("../lib", __FILE__)
|
5
5
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
6
|
-
require "
|
6
|
+
require "view_component/version"
|
7
7
|
|
8
8
|
Gem::Specification.new do |spec|
|
9
9
|
spec.name = "actionview-component"
|
10
|
-
spec.version =
|
10
|
+
spec.version = ViewComponent::VERSION::STRING
|
11
11
|
spec.authors = ["GitHub Open Source"]
|
12
12
|
spec.email = ["opensource+actionview-component@github.com"]
|
13
13
|
|
@@ -15,7 +15,7 @@ class Rails::ComponentsController < Rails::ApplicationController # :nodoc:
|
|
15
15
|
end
|
16
16
|
|
17
17
|
def index
|
18
|
-
@previews =
|
18
|
+
@previews = ViewComponent::Preview.all
|
19
19
|
@page_title = "Component Previews"
|
20
20
|
# rubocop:disable GitHub/RailsControllerRenderPathsExist
|
21
21
|
render "components/index"
|
@@ -42,16 +42,16 @@ class Rails::ComponentsController < Rails::ApplicationController # :nodoc:
|
|
42
42
|
private
|
43
43
|
|
44
44
|
def show_previews? # :doc:
|
45
|
-
|
45
|
+
ViewComponent::Base.show_previews
|
46
46
|
end
|
47
47
|
|
48
48
|
def find_preview # :doc:
|
49
49
|
candidates = []
|
50
50
|
params[:path].to_s.scan(%r{/|$}) { candidates << $` }
|
51
|
-
preview = candidates.detect { |candidate|
|
51
|
+
preview = candidates.detect { |candidate| ViewComponent::Preview.exists?(candidate) }
|
52
52
|
|
53
53
|
if preview
|
54
|
-
@preview =
|
54
|
+
@preview = ViewComponent::Preview.find(preview)
|
55
55
|
else
|
56
56
|
raise AbstractController::ActionNotFound, "Component preview '#{params[:path]}' not found"
|
57
57
|
end
|
@@ -1,24 +1,4 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "active_model"
|
4
|
-
require "
|
5
|
-
require "active_support/dependencies/autoload"
|
6
|
-
require "action_view/component/engine"
|
7
|
-
|
8
|
-
module ActionView
|
9
|
-
module Component
|
10
|
-
extend ActiveSupport::Autoload
|
11
|
-
|
12
|
-
autoload :Base
|
13
|
-
autoload :Conversion
|
14
|
-
autoload :Preview
|
15
|
-
autoload :Previewable
|
16
|
-
autoload :TestHelpers
|
17
|
-
autoload :TestCase
|
18
|
-
autoload :RenderMonkeyPatch
|
19
|
-
autoload :RenderingMonkeyPatch
|
20
|
-
autoload :TemplateError
|
21
|
-
end
|
22
|
-
end
|
23
|
-
|
24
|
-
ActiveModel::Conversion.include ActionView::Component::Conversion
|
4
|
+
require "view_component"
|
@@ -1,274 +1,13 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "active_support/configurable"
|
4
|
-
|
5
3
|
module ActionView
|
6
4
|
module Component
|
7
|
-
class Base <
|
5
|
+
class Base < ViewComponent::Base
|
8
6
|
include ActiveModel::Validations
|
9
|
-
include ActiveSupport::Configurable
|
10
|
-
include ActionView::Component::Previewable
|
11
|
-
|
12
|
-
delegate :form_authenticity_token, :protect_against_forgery?, to: :helpers
|
13
|
-
|
14
|
-
class_attribute :content_areas, default: []
|
15
|
-
self.content_areas = [] # default doesn't work until Rails 5.2
|
16
|
-
|
17
|
-
# Entrypoint for rendering components.
|
18
|
-
#
|
19
|
-
# view_context: ActionView context from calling view
|
20
|
-
# block: optional block to be captured within the view context
|
21
|
-
#
|
22
|
-
# returns HTML that has been escaped by the respective template handler
|
23
|
-
#
|
24
|
-
# Example subclass:
|
25
|
-
#
|
26
|
-
# app/components/my_component.rb:
|
27
|
-
# class MyComponent < ActionView::Component::Base
|
28
|
-
# def initialize(title:)
|
29
|
-
# @title = title
|
30
|
-
# end
|
31
|
-
# end
|
32
|
-
#
|
33
|
-
# app/components/my_component.html.erb
|
34
|
-
# <span title="<%= @title %>">Hello, <%= content %>!</span>
|
35
|
-
#
|
36
|
-
# In use:
|
37
|
-
# <%= render MyComponent.new(title: "greeting") do %>world<% end %>
|
38
|
-
# returns:
|
39
|
-
# <span title="greeting">Hello, world!</span>
|
40
|
-
#
|
41
|
-
def render_in(view_context, &block)
|
42
|
-
self.class.compile!
|
43
|
-
@view_context = view_context
|
44
|
-
@view_renderer ||= view_context.view_renderer
|
45
|
-
@lookup_context ||= view_context.lookup_context
|
46
|
-
@view_flow ||= view_context.view_flow
|
47
|
-
@virtual_path ||= virtual_path
|
48
|
-
@variant = @lookup_context.variants.first
|
49
|
-
|
50
|
-
old_current_template = @current_template
|
51
|
-
@current_template = self
|
52
|
-
|
53
|
-
@content = view_context.capture(self, &block) if block_given?
|
54
|
-
|
55
|
-
before_render_check
|
56
|
-
|
57
|
-
if render?
|
58
|
-
send(self.class.call_method_name(@variant))
|
59
|
-
else
|
60
|
-
""
|
61
|
-
end
|
62
|
-
ensure
|
63
|
-
@current_template = old_current_template
|
64
|
-
end
|
65
7
|
|
66
8
|
def before_render_check
|
67
9
|
validate!
|
68
10
|
end
|
69
|
-
|
70
|
-
def render?
|
71
|
-
true
|
72
|
-
end
|
73
|
-
|
74
|
-
def initialize(*); end
|
75
|
-
|
76
|
-
def render(options = {}, args = {}, &block)
|
77
|
-
if options.is_a?(String) || (options.is_a?(Hash) && options.has_key?(:partial))
|
78
|
-
view_context.render(options, args, &block)
|
79
|
-
else
|
80
|
-
super
|
81
|
-
end
|
82
|
-
end
|
83
|
-
|
84
|
-
def controller
|
85
|
-
@controller ||= view_context.controller
|
86
|
-
end
|
87
|
-
|
88
|
-
# Provides a proxy to access helper methods through
|
89
|
-
def helpers
|
90
|
-
@helpers ||= view_context
|
91
|
-
end
|
92
|
-
|
93
|
-
# Removes the first part of the path and the extension.
|
94
|
-
def virtual_path
|
95
|
-
self.class.source_location.gsub(%r{(.*app/components)|(\.rb)}, "")
|
96
|
-
end
|
97
|
-
|
98
|
-
def view_cache_dependencies
|
99
|
-
[]
|
100
|
-
end
|
101
|
-
|
102
|
-
def format # :nodoc:
|
103
|
-
@variant
|
104
|
-
end
|
105
|
-
|
106
|
-
def with(area, content = nil, &block)
|
107
|
-
unless content_areas.include?(area)
|
108
|
-
raise ArgumentError.new "Unknown content_area '#{area}' - expected one of '#{content_areas}'"
|
109
|
-
end
|
110
|
-
|
111
|
-
if block_given?
|
112
|
-
content = view_context.capture(&block)
|
113
|
-
end
|
114
|
-
|
115
|
-
instance_variable_set("@#{area}".to_sym, content)
|
116
|
-
nil
|
117
|
-
end
|
118
|
-
|
119
|
-
private
|
120
|
-
|
121
|
-
def request
|
122
|
-
@request ||= controller.request
|
123
|
-
end
|
124
|
-
|
125
|
-
attr_reader :content, :view_context
|
126
|
-
|
127
|
-
# The controller used for testing components.
|
128
|
-
# Defaults to ApplicationController. This should be set early
|
129
|
-
# in the initialization process and should be set to a string.
|
130
|
-
mattr_accessor :test_controller
|
131
|
-
@@test_controller = "ApplicationController"
|
132
|
-
|
133
|
-
class << self
|
134
|
-
def inherited(child)
|
135
|
-
child.include Rails.application.routes.url_helpers unless child < Rails.application.routes.url_helpers
|
136
|
-
|
137
|
-
super
|
138
|
-
end
|
139
|
-
|
140
|
-
def call_method_name(variant)
|
141
|
-
if variant.present? && variants.include?(variant)
|
142
|
-
"call_#{variant}"
|
143
|
-
else
|
144
|
-
"call"
|
145
|
-
end
|
146
|
-
end
|
147
|
-
|
148
|
-
def source_location
|
149
|
-
@source_location ||=
|
150
|
-
begin
|
151
|
-
# Require `#initialize` to be defined so that we can use `method#source_location`
|
152
|
-
# to look up the filename of the component.
|
153
|
-
initialize_method = instance_method(:initialize)
|
154
|
-
initialize_method.source_location[0] if initialize_method.owner == self
|
155
|
-
end
|
156
|
-
end
|
157
|
-
|
158
|
-
def compiled?
|
159
|
-
@compiled && ActionView::Base.cache_template_loading
|
160
|
-
end
|
161
|
-
|
162
|
-
def compile!
|
163
|
-
compile(validate: true)
|
164
|
-
end
|
165
|
-
|
166
|
-
# Compile templates to instance methods, assuming they haven't been compiled already.
|
167
|
-
# We could in theory do this on app boot, at least in production environments.
|
168
|
-
# Right now this just compiles the first time the component is rendered.
|
169
|
-
def compile(validate: false)
|
170
|
-
return if compiled?
|
171
|
-
|
172
|
-
if template_errors.present?
|
173
|
-
raise ActionView::Component::TemplateError.new(template_errors) if validate
|
174
|
-
return false
|
175
|
-
end
|
176
|
-
|
177
|
-
templates.each do |template|
|
178
|
-
class_eval <<-RUBY, template[:path], -1
|
179
|
-
def #{call_method_name(template[:variant])}
|
180
|
-
@output_buffer = ActionView::OutputBuffer.new
|
181
|
-
#{compiled_template(template[:path])}
|
182
|
-
end
|
183
|
-
RUBY
|
184
|
-
end
|
185
|
-
|
186
|
-
@compiled = true
|
187
|
-
end
|
188
|
-
|
189
|
-
def variants
|
190
|
-
templates.map { |template| template[:variant] }
|
191
|
-
end
|
192
|
-
|
193
|
-
# we'll eventually want to update this to support other types
|
194
|
-
def type
|
195
|
-
"text/html"
|
196
|
-
end
|
197
|
-
|
198
|
-
def identifier
|
199
|
-
source_location
|
200
|
-
end
|
201
|
-
|
202
|
-
def with_content_areas(*areas)
|
203
|
-
if areas.include?(:content)
|
204
|
-
raise ArgumentError.new ":content is a reserved content area name. Please use another name, such as ':body'"
|
205
|
-
end
|
206
|
-
attr_reader *areas
|
207
|
-
self.content_areas = areas
|
208
|
-
end
|
209
|
-
|
210
|
-
private
|
211
|
-
|
212
|
-
def matching_views_in_source_location
|
213
|
-
return [] unless source_location
|
214
|
-
(Dir["#{source_location.chomp(File.extname(source_location))}.*{#{ActionView::Template.template_handler_extensions.join(',')}}"] - [source_location])
|
215
|
-
end
|
216
|
-
|
217
|
-
def templates
|
218
|
-
@templates ||=
|
219
|
-
matching_views_in_source_location.each_with_object([]) do |path, memo|
|
220
|
-
pieces = File.basename(path).split(".")
|
221
|
-
|
222
|
-
memo << {
|
223
|
-
path: path,
|
224
|
-
variant: pieces.second.split("+").second&.to_sym,
|
225
|
-
handler: pieces.last
|
226
|
-
}
|
227
|
-
end
|
228
|
-
end
|
229
|
-
|
230
|
-
def template_errors
|
231
|
-
@template_errors ||=
|
232
|
-
begin
|
233
|
-
errors = []
|
234
|
-
if source_location.nil?
|
235
|
-
# Require `#initialize` to be defined so that we can use `method#source_location`
|
236
|
-
# to look up the filename of the component.
|
237
|
-
errors << "#{self} must implement #initialize."
|
238
|
-
end
|
239
|
-
|
240
|
-
errors << "Could not find a template file for #{self}." if templates.empty?
|
241
|
-
|
242
|
-
if templates.count { |template| template[:variant].nil? } > 1
|
243
|
-
errors << "More than one template found for #{self}. There can only be one default template file per component."
|
244
|
-
end
|
245
|
-
|
246
|
-
invalid_variants = templates
|
247
|
-
.group_by { |template| template[:variant] }
|
248
|
-
.map { |variant, grouped| variant if grouped.length > 1 }
|
249
|
-
.compact
|
250
|
-
.sort
|
251
|
-
|
252
|
-
unless invalid_variants.empty?
|
253
|
-
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."
|
254
|
-
end
|
255
|
-
errors
|
256
|
-
end
|
257
|
-
end
|
258
|
-
|
259
|
-
def compiled_template(file_path)
|
260
|
-
handler = ActionView::Template.handler_for_extension(File.extname(file_path).gsub(".", ""))
|
261
|
-
template = File.read(file_path)
|
262
|
-
|
263
|
-
if handler.method(:call).parameters.length > 1
|
264
|
-
handler.call(self, template)
|
265
|
-
else # remove before upstreaming into Rails
|
266
|
-
handler.call(OpenStruct.new(source: template, identifier: identifier, type: type))
|
267
|
-
end
|
268
|
-
end
|
269
|
-
end
|
270
|
-
|
271
|
-
ActiveSupport.run_load_hooks(:action_view_component, self)
|
272
11
|
end
|
273
12
|
end
|
274
13
|
end
|