actionview-component 1.5.2 → 1.7.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/ISSUE_TEMPLATE +30 -0
- data/.github/PULL_REQUEST_TEMPLATE +19 -0
- data/.gitignore +1 -0
- data/CHANGELOG.md +90 -0
- data/Gemfile.lock +13 -2
- data/README.md +217 -9
- data/actionview-component.gemspec +1 -0
- data/lib/action_view/component.rb +20 -2
- data/lib/action_view/component/base.rb +82 -39
- data/lib/action_view/component/conversion.rb +11 -0
- data/lib/action_view/component/preview.rb +8 -27
- data/lib/action_view/component/previewable.rb +27 -0
- data/lib/action_view/component/railtie.rb +16 -13
- data/lib/action_view/component/render_monkey_patch.rb +29 -0
- data/lib/action_view/component/template_error.rb +11 -0
- data/lib/action_view/component/test_case.rb +11 -0
- data/lib/action_view/component/test_helpers.rb +1 -1
- data/lib/action_view/component/version.rb +2 -2
- data/lib/rails/generators/rspec/component_generator.rb +19 -0
- data/lib/rails/generators/rspec/templates/component_spec.rb.tt +13 -0
- data/lib/rails/generators/test_unit/templates/component_test.rb.tt +2 -4
- metadata +25 -3
- data/lib/action_view/component/monkey_patch.rb +0 -27
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: '085434585e1e3fa1493bbc7bd90439b1645015acd66a713a33545c0eb4677dc4'
|
4
|
+
data.tar.gz: 34ec8d8f390471689fbd1085c94885b74c4f9a1b0b7d9b78650dae506520dd13
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 89e3c7d8b6c1d67a29f2ad67a032e424c42f57dd719d5b8cbde01b167385d9d2aeb5feb8fd77fcd9284e0a33da828b935edeebbb758c7cd68951178eea2d3b14
|
7
|
+
data.tar.gz: bc76f28233c72f7d5ff3a818429e8c763649db3ec46e406b554070cd456b73ae923905ecb94d3b37949ca5c6b0960890594a341e72a9755343462cf4c5c24cff
|
@@ -0,0 +1,30 @@
|
|
1
|
+
<!-- **** Filing a Feature Request? Include these sections. **** -->
|
2
|
+
|
3
|
+
### Feature request
|
4
|
+
<!-- Provide a summary of the behavior. -->
|
5
|
+
|
6
|
+
### Motivation
|
7
|
+
|
8
|
+
<!-- What would you like to do with this feature? Can you provide
|
9
|
+
context or references to similar behavior in other libraries. -->
|
10
|
+
|
11
|
+
|
12
|
+
|
13
|
+
|
14
|
+
|
15
|
+
<!-- **** Filing a Bug Report? Include these sections. **** -->
|
16
|
+
|
17
|
+
### Steps to reproduce
|
18
|
+
<!-- Provide an series of steps or, better yet, a link to a repo to
|
19
|
+
demonstrate the bug you've identified. -->
|
20
|
+
|
21
|
+
### Expected behavior
|
22
|
+
<!-- Tell us what should happen -->
|
23
|
+
|
24
|
+
### Actual behavior
|
25
|
+
<!-- Tell us what happens instead -->
|
26
|
+
|
27
|
+
### System configuration
|
28
|
+
**Rails version**:
|
29
|
+
|
30
|
+
**Ruby version**:
|
@@ -0,0 +1,19 @@
|
|
1
|
+
<!-- https://github.com/github/actionview-component/blob/master/CONTRIBUTING.md#submitting-a-pull-request -->
|
2
|
+
|
3
|
+
### Summary
|
4
|
+
|
5
|
+
<!-- Provide a general description of the code changes in your pull
|
6
|
+
request... were there any bugs you had fixed? If so, mention them. If
|
7
|
+
these bugs have open GitHub issues, be sure to tag them here as well,
|
8
|
+
to keep the conversation linked together. -->
|
9
|
+
|
10
|
+
### Other Information
|
11
|
+
|
12
|
+
<!-- If there's anything else that's important and relevant to your pull
|
13
|
+
request, mention that information here. This could include
|
14
|
+
benchmarks, or other information.
|
15
|
+
|
16
|
+
If you are updating any of the CHANGELOG files or are asked to update the
|
17
|
+
CHANGELOG files by reviewers, please add the CHANGELOG entry at the top of the file.
|
18
|
+
|
19
|
+
Thanks for contributing to actionview-component! -->
|
data/.gitignore
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,93 @@
|
|
1
|
+
# v1.7.0
|
2
|
+
|
3
|
+
* Simplify validation of templates and compilation.
|
4
|
+
|
5
|
+
*Jon Palmer*
|
6
|
+
|
7
|
+
* Add support for multiple content areas.
|
8
|
+
|
9
|
+
*Jon Palmer*
|
10
|
+
|
11
|
+
# v1.6.2
|
12
|
+
|
13
|
+
* Fix Uninitialized Constant error.
|
14
|
+
|
15
|
+
*Jon Palmer*
|
16
|
+
|
17
|
+
* Add basic github issue and PR templates.
|
18
|
+
|
19
|
+
*Dylan Clark*
|
20
|
+
|
21
|
+
* Update readme phrasing around previews.
|
22
|
+
|
23
|
+
*Justin Coyne*
|
24
|
+
|
25
|
+
# v1.6.1
|
26
|
+
|
27
|
+
* Allow Previews to have no layout.
|
28
|
+
|
29
|
+
*Jon Palmer*
|
30
|
+
|
31
|
+
* Bump rack from 2.0.7 to 2.0.8.
|
32
|
+
|
33
|
+
*Dependabot*
|
34
|
+
|
35
|
+
* Compile components on application boot when eager loading is enabled.
|
36
|
+
|
37
|
+
*Joel Hawksley*
|
38
|
+
|
39
|
+
* Previews support content blocks.
|
40
|
+
|
41
|
+
*Cesario Uy*
|
42
|
+
|
43
|
+
* Follow Rails conventions. (refactor)
|
44
|
+
|
45
|
+
*Rainer Borene*
|
46
|
+
|
47
|
+
* Fix edge case issue with extracting variants from less conventional source_locations.
|
48
|
+
|
49
|
+
*Ryan Workman*
|
50
|
+
|
51
|
+
# v1.6.0
|
52
|
+
|
53
|
+
* Avoid dropping elements in the render_inline test helper.
|
54
|
+
|
55
|
+
*@dark-panda*
|
56
|
+
|
57
|
+
* Add test for helpers.asset_url.
|
58
|
+
|
59
|
+
*Christopher Coleman*
|
60
|
+
|
61
|
+
* Add rudimentary compatibility with better_html.
|
62
|
+
|
63
|
+
*Joel Hawksley*
|
64
|
+
|
65
|
+
* Template-less variants fall back to default template.
|
66
|
+
|
67
|
+
*Asger Behncke Jacobsen*, *Cesario Uy*
|
68
|
+
|
69
|
+
* Generated tests use new naming convention.
|
70
|
+
|
71
|
+
*Simon Træls Ravn*
|
72
|
+
|
73
|
+
* Eliminate sqlite dependency.
|
74
|
+
|
75
|
+
*Simon Dawson*
|
76
|
+
|
77
|
+
* Add support for rendering components via #to_component_class
|
78
|
+
|
79
|
+
*Vinicius Stock*
|
80
|
+
|
81
|
+
# v1.5.3
|
82
|
+
|
83
|
+
* Add support for RSpec to generators.
|
84
|
+
|
85
|
+
*Dylan Clark, Ryan Workman*
|
86
|
+
|
87
|
+
* Require controllers as part of setting autoload paths.
|
88
|
+
|
89
|
+
*Joel Hawksley*
|
90
|
+
|
1
91
|
# v1.5.2
|
2
92
|
|
3
93
|
* Disable eager loading initializer.
|
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
actionview-component (1.
|
4
|
+
actionview-component (1.7.0)
|
5
5
|
|
6
6
|
GEM
|
7
7
|
remote: https://rubygems.org/
|
@@ -62,6 +62,14 @@ GEM
|
|
62
62
|
tzinfo (~> 1.1)
|
63
63
|
zeitwerk (~> 2.1, >= 2.1.8)
|
64
64
|
ast (2.4.0)
|
65
|
+
better_html (1.0.14)
|
66
|
+
actionview (>= 4.0)
|
67
|
+
activesupport (>= 4.0)
|
68
|
+
ast (~> 2.0)
|
69
|
+
erubi (~> 1.4)
|
70
|
+
html_tokenizer (~> 0.0.6)
|
71
|
+
parser (>= 2.4)
|
72
|
+
smart_properties
|
65
73
|
builder (3.2.3)
|
66
74
|
concurrent-ruby (1.1.5)
|
67
75
|
crass (1.0.5)
|
@@ -71,6 +79,7 @@ GEM
|
|
71
79
|
haml (5.1.2)
|
72
80
|
temple (>= 0.8.0)
|
73
81
|
tilt
|
82
|
+
html_tokenizer (0.0.7)
|
74
83
|
i18n (1.6.0)
|
75
84
|
concurrent-ruby (~> 1.0)
|
76
85
|
jaro_winkler (1.5.3)
|
@@ -92,7 +101,7 @@ GEM
|
|
92
101
|
parallel (1.17.0)
|
93
102
|
parser (2.6.3.0)
|
94
103
|
ast (~> 2.4.0)
|
95
|
-
rack (2.0.
|
104
|
+
rack (2.0.8)
|
96
105
|
rack-test (1.1.0)
|
97
106
|
rack (>= 1.0, < 3)
|
98
107
|
rails (6.0.0)
|
@@ -139,6 +148,7 @@ GEM
|
|
139
148
|
slim (4.0.1)
|
140
149
|
temple (>= 0.7.6, < 0.9)
|
141
150
|
tilt (>= 2.0.6, < 2.1)
|
151
|
+
smart_properties (1.15.0)
|
142
152
|
sprockets (3.7.2)
|
143
153
|
concurrent-ruby (~> 1.0)
|
144
154
|
rack (> 1, < 3)
|
@@ -163,6 +173,7 @@ PLATFORMS
|
|
163
173
|
|
164
174
|
DEPENDENCIES
|
165
175
|
actionview-component!
|
176
|
+
better_html (~> 1)
|
166
177
|
bundler (>= 1.14)
|
167
178
|
haml (~> 5)
|
168
179
|
minitest (= 5.1.0)
|
data/README.md
CHANGED
@@ -7,7 +7,7 @@
|
|
7
7
|
|
8
8
|
This gem is meant to serve as a precursor to upstreaming the `ActionView::Component` class into Rails. It also serves to enable the usage of `ActionView::Component` in older versions of Rails.
|
9
9
|
|
10
|
-
Preliminary support for rendering components was merged into Rails `6.1.0.alpha` in https://github.com/rails/rails/pull/36388. Assuming `ActionView::Component` makes it into Rails
|
10
|
+
Preliminary support for rendering components was merged into Rails `6.1.0.alpha` in https://github.com/rails/rails/pull/36388. Assuming `ActionView::Component` makes it into Rails, this gem will then exist to serve as a backport.
|
11
11
|
|
12
12
|
## Design philosophy
|
13
13
|
|
@@ -32,7 +32,7 @@ $ bundle
|
|
32
32
|
In `config/application.rb`, add:
|
33
33
|
|
34
34
|
```bash
|
35
|
-
require "action_view/component"
|
35
|
+
require "action_view/component/railtie"
|
36
36
|
```
|
37
37
|
|
38
38
|
## Guide
|
@@ -163,6 +163,24 @@ Components can be rendered via:
|
|
163
163
|
|
164
164
|
`render(component: TestComponent, locals: { foo: :bar })`
|
165
165
|
|
166
|
+
**Rendering components through models**
|
167
|
+
|
168
|
+
Passing model instances will cause `render` to look for its respective component class.
|
169
|
+
|
170
|
+
The component is instantiated with the rendered model instance.
|
171
|
+
|
172
|
+
Example for a `Post` model:
|
173
|
+
|
174
|
+
`render(@post)`
|
175
|
+
|
176
|
+
```ruby
|
177
|
+
class PostComponent < ActionView::Component::Base
|
178
|
+
def initialize(post)
|
179
|
+
@post = post
|
180
|
+
end
|
181
|
+
end
|
182
|
+
```
|
183
|
+
|
166
184
|
The following syntax has been deprecated and will be removed in v2.0.0:
|
167
185
|
|
168
186
|
`render(TestComponent.new(foo: :bar))`
|
@@ -181,17 +199,183 @@ An error will be raised:
|
|
181
199
|
|
182
200
|
`ActiveModel::ValidationError: Validation failed: Title can't be blank`
|
183
201
|
|
202
|
+
#### Content Areas
|
203
|
+
|
204
|
+
|
205
|
+
A component can declare additional content areas to be rendered in the component. For example:
|
206
|
+
|
207
|
+
`app/components/modal_component.rb`:
|
208
|
+
```ruby
|
209
|
+
class ModalComponent < ActionView::Component::Base
|
210
|
+
validates :user, :header, :body, presence: true
|
211
|
+
|
212
|
+
with_content_areas :header, :body
|
213
|
+
|
214
|
+
def initialize(user:)
|
215
|
+
@user = user
|
216
|
+
end
|
217
|
+
end
|
218
|
+
```
|
219
|
+
|
220
|
+
`app/components/modal_component.html.erb`:
|
221
|
+
```erb
|
222
|
+
<div class="modal">
|
223
|
+
<div class="header"><%= header %></div>
|
224
|
+
<div class="body"><%= body %>"></div>
|
225
|
+
</div>
|
226
|
+
```
|
227
|
+
|
228
|
+
We can render it in a view as:
|
229
|
+
|
230
|
+
```erb
|
231
|
+
<%= render(ModalComponent, user: {name: 'Jane'}) do |component| %>
|
232
|
+
<% component.with(:header) do %>
|
233
|
+
Hello <%= user[:name] %>
|
234
|
+
<% end %>
|
235
|
+
<% component.with(:body) do %>
|
236
|
+
<p>Have a great day.</p>
|
237
|
+
<% end %>
|
238
|
+
<% end %>
|
239
|
+
```
|
240
|
+
|
241
|
+
Which returns:
|
242
|
+
|
243
|
+
```html
|
244
|
+
<div class="modal">
|
245
|
+
<div class="header">Hello Jane</div>
|
246
|
+
<div class="body"><p>Have a great day.</p></div>
|
247
|
+
</div>
|
248
|
+
```
|
249
|
+
|
250
|
+
Content for content areas can be passed as arguments to the render method or as named blocks passed to the `with` method.
|
251
|
+
This allows a few different combinations of ways to render the component:
|
252
|
+
|
253
|
+
##### Required render argument optionally overridden or wrapped by a named block
|
254
|
+
|
255
|
+
`app/components/modal_component.rb`:
|
256
|
+
```ruby
|
257
|
+
class ModalComponent < ActionView::Component::Base
|
258
|
+
validates :header, :body, presence: true
|
259
|
+
|
260
|
+
with_content_areas :header, :body
|
261
|
+
|
262
|
+
def initialize(header:)
|
263
|
+
@header = header
|
264
|
+
end
|
265
|
+
end
|
266
|
+
```
|
267
|
+
|
268
|
+
```erb
|
269
|
+
<%= render(ModalComponent, header: "Hi!") do |component| %>
|
270
|
+
<% help_enabled? && component.with(:header) do %>
|
271
|
+
<span class="help_icon"><%= component.header %></span>
|
272
|
+
<% end %>
|
273
|
+
<% component.with(:body) do %>
|
274
|
+
<p>Have a great day.</p>
|
275
|
+
<% end %>
|
276
|
+
<% end %>
|
277
|
+
```
|
278
|
+
|
279
|
+
##### Required argument passed by render argument or by named block
|
280
|
+
|
281
|
+
`app/components/modal_component.rb`:
|
282
|
+
```ruby
|
283
|
+
class ModalComponent < ActionView::Component::Base
|
284
|
+
validates :header, :body, presence: true
|
285
|
+
|
286
|
+
with_content_areas :header, :body
|
287
|
+
|
288
|
+
def initialize(header: nil)
|
289
|
+
@header = header
|
290
|
+
end
|
291
|
+
end
|
292
|
+
```
|
293
|
+
|
294
|
+
`app/views/render_arg.html.erb`:
|
295
|
+
```erb
|
296
|
+
<%= render(ModalComponent, header: "Hi!") do |component| %>
|
297
|
+
<% component.with(:body) do %>
|
298
|
+
<p>Have a great day.</p>
|
299
|
+
<% end %>
|
300
|
+
<% end %>
|
301
|
+
```
|
302
|
+
|
303
|
+
`app/views/with_block.html.erb`:
|
304
|
+
```erb
|
305
|
+
<%= render(ModalComponent) do |component| %>
|
306
|
+
<% component.with(:header) do %>
|
307
|
+
<span class="help_icon">Hello</span>
|
308
|
+
<% end %>
|
309
|
+
<% component.with(:body) do %>
|
310
|
+
<p>Have a great day.</p>
|
311
|
+
<% end %>
|
312
|
+
<% end %>
|
313
|
+
```
|
314
|
+
|
315
|
+
##### Optional argument passed by render argument, by named block, or neither
|
316
|
+
|
317
|
+
`app/components/modal_component.rb`:
|
318
|
+
```ruby
|
319
|
+
class ModalComponent < ActionView::Component::Base
|
320
|
+
validates :body, presence: true
|
321
|
+
|
322
|
+
with_content_areas :header, :body
|
323
|
+
|
324
|
+
def initialize(header: nil)
|
325
|
+
@header = header
|
326
|
+
end
|
327
|
+
end
|
328
|
+
```
|
329
|
+
|
330
|
+
`app/components/modal_component.html.erb`:
|
331
|
+
```erb
|
332
|
+
<div class="modal">
|
333
|
+
<% if header %>
|
334
|
+
<div class="header"><%= header %></div>
|
335
|
+
<% end %>
|
336
|
+
<div class="body"><%= body %>"></div>
|
337
|
+
</div>
|
338
|
+
```
|
339
|
+
|
340
|
+
`app/views/render_arg.html.erb`:
|
341
|
+
```erb
|
342
|
+
<%= render(ModalComponent, header: "Hi!") do |component| %>
|
343
|
+
<% component.with(:body) do %>
|
344
|
+
<p>Have a great day.</p>
|
345
|
+
<% end %>
|
346
|
+
<% end %>
|
347
|
+
```
|
348
|
+
|
349
|
+
`app/views/with_block.html.erb`:
|
350
|
+
```erb
|
351
|
+
<%= render(ModalComponent) do |component| %>
|
352
|
+
<% component.with(:header) do %>
|
353
|
+
<span class="help_icon">Hello</span>
|
354
|
+
<% end %>
|
355
|
+
<% component.with(:body) do %>
|
356
|
+
<p>Have a great day.</p>
|
357
|
+
<% end %>
|
358
|
+
<% end %>
|
359
|
+
```
|
360
|
+
|
361
|
+
`app/views/no_header.html.erb`:
|
362
|
+
```erb
|
363
|
+
<%= render(ModalComponent) do |component| %>
|
364
|
+
<% component.with(:body) do %>
|
365
|
+
<p>Have a great day.</p>
|
366
|
+
<% end %>
|
367
|
+
<% end %>
|
368
|
+
```
|
369
|
+
|
184
370
|
### Testing
|
185
371
|
|
186
372
|
Components are unit tested directly. The `render_inline` test helper wraps the result in `Nokogiri.HTML`, allowing us to test the component above as:
|
187
373
|
|
188
374
|
```ruby
|
189
|
-
require "action_view/component/
|
375
|
+
require "action_view/component/test_case"
|
190
376
|
|
191
|
-
class MyComponentTest <
|
192
|
-
|
193
|
-
|
194
|
-
def test_render_component
|
377
|
+
class MyComponentTest < ActionView::Component::TestCase
|
378
|
+
test "render component" do
|
195
379
|
assert_equal(
|
196
380
|
%(<span title="my title">Hello, World!</span>),
|
197
381
|
render_inline(TestComponent, title: "my title") { "Hello, World!" }.to_html
|
@@ -207,7 +391,7 @@ In general, we’ve found it makes the most sense to test components based on th
|
|
207
391
|
To test a specific variant you can wrap your test with the `with_variant` helper method as:
|
208
392
|
|
209
393
|
```ruby
|
210
|
-
|
394
|
+
test "render component for tablet" do
|
211
395
|
with_variant :tablet do
|
212
396
|
assert_equal(
|
213
397
|
%(<span title="my title">Hello, tablets!</span>),
|
@@ -218,7 +402,7 @@ end
|
|
218
402
|
```
|
219
403
|
|
220
404
|
### Previewing Components
|
221
|
-
`ActionView::Component::Preview`
|
405
|
+
`ActionView::Component::Preview` provides a way to see how components look by visiting a special URL that renders them.
|
222
406
|
In the previous example, the preview class for `TestComponent` would be called `TestComponentPreview` and located in `test/components/previews/test_component_preview.rb`.
|
223
407
|
To see the preview of the component with a given title, implement a method that renders the component.
|
224
408
|
You can define as many examples as you want:
|
@@ -260,6 +444,30 @@ For example, if you want to use `lib/component_previews`, set the following in `
|
|
260
444
|
config.action_view_component.preview_path = "#{Rails.root}/lib/component_previews"
|
261
445
|
```
|
262
446
|
|
447
|
+
### Setting up RSpec
|
448
|
+
|
449
|
+
If you're using RSpec, you can configure component specs to have access to test helpers. Add the following to
|
450
|
+
`spec/rails_helper.rb`:
|
451
|
+
|
452
|
+
```ruby
|
453
|
+
require "action_view/component/test_helpers"
|
454
|
+
|
455
|
+
RSpec.configure do |config|
|
456
|
+
# ...
|
457
|
+
|
458
|
+
# Ensure that the test helpers are available in component specs
|
459
|
+
config.include ActionView::Component::TestHelpers, type: :component
|
460
|
+
end
|
461
|
+
```
|
462
|
+
|
463
|
+
Specs created by the generator should now have access to test helpers like `render_inline`.
|
464
|
+
|
465
|
+
To use component previews, set the following in `config/application.rb`:
|
466
|
+
|
467
|
+
```ruby
|
468
|
+
config.action_view_component.preview_path = "#{Rails.root}/spec/components/previews"
|
469
|
+
```
|
470
|
+
|
263
471
|
## Frequently Asked Questions
|
264
472
|
|
265
473
|
### Can I use other templating languages besides ERB?
|
@@ -39,6 +39,7 @@ Gem::Specification.new do |spec|
|
|
39
39
|
spec.add_development_dependency "minitest", "= 5.1.0"
|
40
40
|
spec.add_development_dependency "haml", "~> 5"
|
41
41
|
spec.add_development_dependency "slim", "~> 4.0"
|
42
|
+
spec.add_development_dependency "better_html", "~> 1"
|
42
43
|
spec.add_development_dependency "rubocop", "= 0.74"
|
43
44
|
spec.add_development_dependency "rubocop-github", "~> 0.13.0"
|
44
45
|
end
|
@@ -1,5 +1,23 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "
|
4
|
-
require "action_view
|
3
|
+
require "active_model"
|
4
|
+
require "action_view"
|
5
|
+
require "active_support/dependencies/autoload"
|
5
6
|
require "action_view/component/railtie"
|
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 :TemplateError
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
ActiveModel::Conversion.include ActionView::Component::Conversion
|
@@ -1,20 +1,18 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "active_model"
|
4
|
-
require "action_view"
|
5
3
|
require "active_support/configurable"
|
6
|
-
require_relative "preview"
|
7
4
|
|
8
5
|
module ActionView
|
9
6
|
module Component
|
10
7
|
class Base < ActionView::Base
|
11
8
|
include ActiveModel::Validations
|
12
9
|
include ActiveSupport::Configurable
|
13
|
-
include ActionView::Component::
|
10
|
+
include ActionView::Component::Previewable
|
14
11
|
|
15
12
|
delegate :form_authenticity_token, :protect_against_forgery?, to: :helpers
|
16
13
|
|
17
|
-
|
14
|
+
class_attribute :content_areas, default: []
|
15
|
+
self.content_areas = [] # default doesn't work until Rails 5.2
|
18
16
|
|
19
17
|
# Entrypoint for rendering components. Called by ActionView::Base#render.
|
20
18
|
#
|
@@ -42,7 +40,7 @@ module ActionView
|
|
42
40
|
# <span title="greeting">Hello, world!</span>
|
43
41
|
#
|
44
42
|
def render_in(view_context, *args, &block)
|
45
|
-
self.class.compile
|
43
|
+
self.class.compile!
|
46
44
|
@view_context = view_context
|
47
45
|
@view_renderer ||= view_context.view_renderer
|
48
46
|
@lookup_context ||= view_context.lookup_context
|
@@ -52,7 +50,8 @@ module ActionView
|
|
52
50
|
old_current_template = @current_template
|
53
51
|
@current_template = self
|
54
52
|
|
55
|
-
@content = view_context.capture(&block) if block_given?
|
53
|
+
@content = view_context.capture(self, &block) if block_given?
|
54
|
+
|
56
55
|
validate!
|
57
56
|
|
58
57
|
send(self.class.call_method_name(@variant))
|
@@ -92,14 +91,21 @@ module ActionView
|
|
92
91
|
@variant
|
93
92
|
end
|
94
93
|
|
95
|
-
|
94
|
+
def with(area, content = nil, &block)
|
95
|
+
unless content_areas.include?(area)
|
96
|
+
raise ArgumentError.new "Unknown content_area '#{area}' - expected one of '#{content_areas}'"
|
97
|
+
end
|
96
98
|
|
97
|
-
|
98
|
-
|
99
|
+
if block_given?
|
100
|
+
content = view_context.capture(&block)
|
101
|
+
end
|
99
102
|
|
100
|
-
|
103
|
+
instance_variable_set("@#{area}".to_sym, content)
|
104
|
+
nil
|
101
105
|
end
|
102
106
|
|
107
|
+
private
|
108
|
+
|
103
109
|
def request
|
104
110
|
@request ||= controller.request
|
105
111
|
end
|
@@ -114,7 +120,7 @@ module ActionView
|
|
114
120
|
end
|
115
121
|
|
116
122
|
def call_method_name(variant)
|
117
|
-
if variant.present?
|
123
|
+
if variant.present? && variants.include?(variant)
|
118
124
|
"call_#{variant}"
|
119
125
|
else
|
120
126
|
"call"
|
@@ -122,25 +128,39 @@ module ActionView
|
|
122
128
|
end
|
123
129
|
|
124
130
|
def source_location
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
131
|
+
@source_location ||=
|
132
|
+
begin
|
133
|
+
# Require #initialize to be defined so that we can use
|
134
|
+
# method#source_location to look up the file name
|
135
|
+
# of the component.
|
136
|
+
#
|
137
|
+
# If we were able to only support Ruby 2.7+,
|
138
|
+
# We could just use Module#const_source_location,
|
139
|
+
# rendering this unnecessary.
|
140
|
+
#
|
141
|
+
initialize_method = instance_method(:initialize)
|
142
|
+
initialize_method.source_location[0] if initialize_method.owner == self
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
def compiled?
|
147
|
+
@compiled && ActionView::Base.cache_template_loading
|
148
|
+
end
|
133
149
|
|
134
|
-
|
150
|
+
def compile!
|
151
|
+
compile(validate: true)
|
135
152
|
end
|
136
153
|
|
137
154
|
# Compile templates to instance methods, assuming they haven't been compiled already.
|
138
155
|
# We could in theory do this on app boot, at least in production environments.
|
139
156
|
# Right now this just compiles the first time the component is rendered.
|
140
|
-
def compile
|
141
|
-
return if
|
157
|
+
def compile(validate: false)
|
158
|
+
return if compiled?
|
142
159
|
|
143
|
-
|
160
|
+
if template_errors.present?
|
161
|
+
raise ActionView::Component::TemplateError.new(template_errors) if validate
|
162
|
+
return false
|
163
|
+
end
|
144
164
|
|
145
165
|
templates.each do |template|
|
146
166
|
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
@@ -164,36 +184,59 @@ module ActionView
|
|
164
184
|
end
|
165
185
|
|
166
186
|
def identifier
|
167
|
-
|
187
|
+
source_location
|
188
|
+
end
|
189
|
+
|
190
|
+
def with_content_areas(*areas)
|
191
|
+
if areas.include?(:content)
|
192
|
+
raise ArgumentError.new ":content is a reserved content area name. Please use another name, such as ':body'"
|
193
|
+
end
|
194
|
+
attr_reader *areas
|
195
|
+
self.content_areas = areas
|
168
196
|
end
|
169
197
|
|
170
198
|
private
|
171
199
|
|
200
|
+
def matching_views_in_source_location
|
201
|
+
return [] unless source_location
|
202
|
+
(Dir["#{source_location.sub(/#{File.extname(source_location)}$/, '')}.*{#{ActionView::Template.template_handler_extensions.join(',')}}"] - [source_location])
|
203
|
+
end
|
204
|
+
|
172
205
|
def templates
|
173
206
|
@templates ||=
|
174
|
-
|
207
|
+
matching_views_in_source_location.each_with_object([]) do |path, memo|
|
208
|
+
pieces = File.basename(path).split(".")
|
209
|
+
|
175
210
|
memo << {
|
176
211
|
path: path,
|
177
|
-
variant:
|
178
|
-
handler:
|
212
|
+
variant: pieces.second.split("+").second&.to_sym,
|
213
|
+
handler: pieces.last
|
179
214
|
}
|
180
215
|
end
|
181
216
|
end
|
182
217
|
|
183
|
-
def
|
184
|
-
|
185
|
-
|
186
|
-
|
218
|
+
def template_errors
|
219
|
+
@template_errors ||=
|
220
|
+
begin
|
221
|
+
errors = []
|
222
|
+
errors << "#{self} must implement #initialize." if source_location.nil?
|
223
|
+
errors << "Could not find a template file for #{self}." if templates.empty?
|
187
224
|
|
188
|
-
|
189
|
-
|
190
|
-
|
225
|
+
if templates.count { |template| template[:variant].nil? } > 1
|
226
|
+
errors << "More than one template found for #{self}. There can only be one default template file per component."
|
227
|
+
end
|
191
228
|
|
192
|
-
|
193
|
-
|
229
|
+
invalid_variants = templates
|
230
|
+
.group_by { |template| template[:variant] }
|
231
|
+
.map { |variant, grouped| variant if grouped.length > 1 }
|
232
|
+
.compact
|
233
|
+
.sort
|
194
234
|
|
195
|
-
|
196
|
-
|
235
|
+
unless invalid_variants.empty?
|
236
|
+
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."
|
237
|
+
end
|
238
|
+
errors
|
239
|
+
end
|
197
240
|
end
|
198
241
|
|
199
242
|
def compiled_template(file_path)
|
@@ -1,37 +1,15 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "active_support/concern"
|
4
3
|
require "active_support/descendants_tracker"
|
5
|
-
require_relative "test_helpers"
|
6
4
|
|
7
5
|
module ActionView
|
8
|
-
module Component
|
9
|
-
module Previews
|
10
|
-
extend ActiveSupport::Concern
|
11
|
-
|
12
|
-
included do
|
13
|
-
# Set the location of component previews through app configuration:
|
14
|
-
#
|
15
|
-
# config.action_view_component.preview_path = "#{Rails.root}/lib/component_previews"
|
16
|
-
#
|
17
|
-
mattr_accessor :preview_path, instance_writer: false
|
18
|
-
|
19
|
-
# Enable or disable component previews through app configuration:
|
20
|
-
#
|
21
|
-
# config.action_view_component.show_previews = true
|
22
|
-
#
|
23
|
-
# Defaults to +true+ for development environment
|
24
|
-
#
|
25
|
-
mattr_accessor :show_previews, instance_writer: false
|
26
|
-
end
|
27
|
-
end
|
28
|
-
|
6
|
+
module Component # :nodoc:
|
29
7
|
class Preview
|
30
8
|
extend ActiveSupport::DescendantsTracker
|
31
9
|
include ActionView::Component::TestHelpers
|
32
10
|
|
33
|
-
def render(component, *locals)
|
34
|
-
render_inline(component, *locals)
|
11
|
+
def render(component, *locals, &block)
|
12
|
+
render_inline(component, *locals, &block)
|
35
13
|
end
|
36
14
|
|
37
15
|
class << self
|
@@ -42,11 +20,14 @@ module ActionView
|
|
42
20
|
end
|
43
21
|
|
44
22
|
# Returns the html of the component in its layout
|
45
|
-
def call(example)
|
23
|
+
def call(example, layout: nil)
|
46
24
|
example_html = new.public_send(example)
|
25
|
+
if layout.nil?
|
26
|
+
layout = @layout.nil? ? "layouts/application" : @layout
|
27
|
+
end
|
47
28
|
|
48
29
|
Rails::ComponentExamplesController.render(template: "examples/show",
|
49
|
-
layout:
|
30
|
+
layout: layout,
|
50
31
|
assigns: { example: example_html })
|
51
32
|
end
|
52
33
|
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/concern"
|
4
|
+
|
5
|
+
module ActionView
|
6
|
+
module Component # :nodoc:
|
7
|
+
module Previewable
|
8
|
+
extend ActiveSupport::Concern
|
9
|
+
|
10
|
+
included do
|
11
|
+
# Set the location of component previews through app configuration:
|
12
|
+
#
|
13
|
+
# config.action_view_component.preview_path = "#{Rails.root}/lib/component_previews"
|
14
|
+
#
|
15
|
+
mattr_accessor :preview_path, instance_writer: false
|
16
|
+
|
17
|
+
# Enable or disable component previews through app configuration:
|
18
|
+
#
|
19
|
+
# config.action_view_component.show_previews = true
|
20
|
+
#
|
21
|
+
# Defaults to +true+ for development environment
|
22
|
+
#
|
23
|
+
mattr_accessor :show_previews, instance_writer: false
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -1,18 +1,13 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "
|
4
|
-
require "
|
3
|
+
require "rails"
|
4
|
+
require "action_view/component"
|
5
5
|
|
6
6
|
module ActionView
|
7
7
|
module Component
|
8
8
|
class Railtie < Rails::Railtie # :nodoc:
|
9
9
|
config.action_view_component = ActiveSupport::OrderedOptions.new
|
10
10
|
|
11
|
-
# Disabled due to issues with ActionView::Component::Base not defining .logger
|
12
|
-
# initializer "action_view_component.logger" do
|
13
|
-
# ActiveSupport.on_load(:action_view_component) { self.logger ||= Rails.logger }
|
14
|
-
# end
|
15
|
-
|
16
11
|
initializer "action_view_component.set_configs" do |app|
|
17
12
|
options = app.config.action_view_component
|
18
13
|
|
@@ -28,6 +23,9 @@ module ActionView
|
|
28
23
|
end
|
29
24
|
|
30
25
|
initializer "action_view_component.set_autoload_paths" do |app|
|
26
|
+
require "railties/lib/rails/components_controller"
|
27
|
+
require "railties/lib/rails/component_examples_controller"
|
28
|
+
|
31
29
|
options = app.config.action_view_component
|
32
30
|
|
33
31
|
if options.show_previews && options.preview_path
|
@@ -35,18 +33,23 @@ module ActionView
|
|
35
33
|
end
|
36
34
|
end
|
37
35
|
|
36
|
+
initializer "action_view_component.eager_load_actions" do
|
37
|
+
ActiveSupport.on_load(:after_initialize) do
|
38
|
+
ActionView::Component::Base.descendants.each(&:compile)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
38
42
|
initializer "action_view_component.compile_config_methods" do
|
39
43
|
ActiveSupport.on_load(:action_view_component) do
|
40
44
|
config.compile_methods! if config.respond_to?(:compile_methods!)
|
41
45
|
end
|
42
46
|
end
|
43
47
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
# end
|
48
|
+
initializer "action_view_component.monkey_patch_render" do
|
49
|
+
ActiveSupport.on_load(:action_view) do
|
50
|
+
ActionView::Base.prepend ActionView::Component::RenderMonkeyPatch
|
51
|
+
end
|
52
|
+
end
|
50
53
|
|
51
54
|
config.after_initialize do |app|
|
52
55
|
options = app.config.action_view_component
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Monkey patch ActionView::Base#render to support ActionView::Component
|
4
|
+
#
|
5
|
+
# A version of this monkey patch was upstreamed in https://github.com/rails/rails/pull/36388
|
6
|
+
# We'll need to upstream an updated version of this eventually.
|
7
|
+
module ActionView
|
8
|
+
module Component
|
9
|
+
module RenderMonkeyPatch # :nodoc:
|
10
|
+
def render(options = {}, args = {}, &block)
|
11
|
+
if options.respond_to?(:render_in)
|
12
|
+
ActiveSupport::Deprecation.warn(
|
13
|
+
"passing component instances (`render MyComponent.new(foo: :bar)`) has been deprecated and will be removed in v2.0.0. Use `render MyComponent, foo: :bar` instead."
|
14
|
+
)
|
15
|
+
|
16
|
+
options.render_in(self, &block)
|
17
|
+
elsif options.is_a?(Class) && options < ActionView::Component::Base
|
18
|
+
options.new(args).render_in(self, &block)
|
19
|
+
elsif options.is_a?(Hash) && options.has_key?(:component)
|
20
|
+
options[:component].new(options[:locals]).render_in(self, &block)
|
21
|
+
elsif options.respond_to?(:to_component_class) && !options.to_component_class.nil?
|
22
|
+
options.to_component_class.new(options).render_in(self, &block)
|
23
|
+
else
|
24
|
+
super
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -4,7 +4,7 @@ module ActionView
|
|
4
4
|
module Component
|
5
5
|
module TestHelpers
|
6
6
|
def render_inline(component, **args, &block)
|
7
|
-
Nokogiri::HTML(controller.view_context.render(component, args, &block))
|
7
|
+
Nokogiri::HTML.fragment(controller.view_context.render(component, args, &block))
|
8
8
|
end
|
9
9
|
|
10
10
|
def controller
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rspec
|
4
|
+
module Generators
|
5
|
+
class ComponentGenerator < ::Rails::Generators::NamedBase
|
6
|
+
source_root File.expand_path("templates", __dir__)
|
7
|
+
|
8
|
+
def create_test_file
|
9
|
+
template "component_spec.rb", File.join("spec/components", "#{file_name}_component_spec.rb")
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def file_name
|
15
|
+
@_file_name ||= super.sub(/_component\z/i, "")
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require "rails_helper"
|
2
|
+
|
3
|
+
RSpec.describe <%= class_name %>Component, type: :component do
|
4
|
+
pending "add some examples to (or delete) #{__FILE__}"
|
5
|
+
|
6
|
+
# it "renders something useful" do
|
7
|
+
# expect(
|
8
|
+
# render_inline(described_class, attr: "value") { "Hello, components!" }.css("p").to_html
|
9
|
+
# ).to include(
|
10
|
+
# "Hello, components!"
|
11
|
+
# )
|
12
|
+
# end
|
13
|
+
end
|
@@ -1,12 +1,10 @@
|
|
1
1
|
require "test_helper"
|
2
2
|
|
3
|
-
class <%= class_name %>ComponentTest <
|
4
|
-
include ActionView::Component::TestHelpers
|
5
|
-
|
3
|
+
class <%= class_name %>ComponentTest < ActionView::Component::TestCase
|
6
4
|
test "component renders something useful" do
|
7
5
|
# assert_equal(
|
8
6
|
# %(<span title="my title">Hello, components!</span>),
|
9
|
-
# render_inline(<%= class_name
|
7
|
+
# render_inline(<%= class_name %>Component, attr: "value") { "Hello, components!" }.css("span").to_html
|
10
8
|
# )
|
11
9
|
end
|
12
10
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: actionview-component
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.7.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- GitHub Open Source
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2020-01-09 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -80,6 +80,20 @@ dependencies:
|
|
80
80
|
- - "~>"
|
81
81
|
- !ruby/object:Gem::Version
|
82
82
|
version: '4.0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: better_html
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '1'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '1'
|
83
97
|
- !ruby/object:Gem::Dependency
|
84
98
|
name: rubocop
|
85
99
|
requirement: !ruby/object:Gem::Requirement
|
@@ -115,6 +129,8 @@ executables: []
|
|
115
129
|
extensions: []
|
116
130
|
extra_rdoc_files: []
|
117
131
|
files:
|
132
|
+
- ".github/ISSUE_TEMPLATE"
|
133
|
+
- ".github/PULL_REQUEST_TEMPLATE"
|
118
134
|
- ".github/workflows/ruby_on_rails.yml"
|
119
135
|
- ".gitignore"
|
120
136
|
- ".rubocop.yml"
|
@@ -129,15 +145,21 @@ files:
|
|
129
145
|
- actionview-component.gemspec
|
130
146
|
- lib/action_view/component.rb
|
131
147
|
- lib/action_view/component/base.rb
|
132
|
-
- lib/action_view/component/
|
148
|
+
- lib/action_view/component/conversion.rb
|
133
149
|
- lib/action_view/component/preview.rb
|
150
|
+
- lib/action_view/component/previewable.rb
|
134
151
|
- lib/action_view/component/railtie.rb
|
152
|
+
- lib/action_view/component/render_monkey_patch.rb
|
153
|
+
- lib/action_view/component/template_error.rb
|
154
|
+
- lib/action_view/component/test_case.rb
|
135
155
|
- lib/action_view/component/test_helpers.rb
|
136
156
|
- lib/action_view/component/version.rb
|
137
157
|
- lib/rails/generators/component/USAGE
|
138
158
|
- lib/rails/generators/component/component_generator.rb
|
139
159
|
- lib/rails/generators/component/templates/component.html.erb.tt
|
140
160
|
- lib/rails/generators/component/templates/component.rb.tt
|
161
|
+
- lib/rails/generators/rspec/component_generator.rb
|
162
|
+
- lib/rails/generators/rspec/templates/component_spec.rb.tt
|
141
163
|
- lib/rails/generators/test_unit/component_generator.rb
|
142
164
|
- lib/rails/generators/test_unit/templates/component_test.rb.tt
|
143
165
|
- lib/railties/lib/rails.rb
|
@@ -1,27 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
# Monkey patch ActionView::Base#render to support ActionView::Component
|
4
|
-
#
|
5
|
-
# A version of this monkey patch was upstreamed in https://github.com/rails/rails/pull/36388
|
6
|
-
# We'll need to upstream an updated version of this eventually.
|
7
|
-
class ActionView::Base
|
8
|
-
module RenderMonkeyPatch
|
9
|
-
def render(options = {}, args = {}, &block)
|
10
|
-
if options.respond_to?(:render_in)
|
11
|
-
ActiveSupport::Deprecation.warn(
|
12
|
-
"passing component instances (`render MyComponent.new(foo: :bar)`) has been deprecated and will be removed in v2.0.0. Use `render MyComponent, foo: :bar` instead."
|
13
|
-
)
|
14
|
-
|
15
|
-
options.render_in(self, &block)
|
16
|
-
elsif options.is_a?(Class) && options < ActionView::Component::Base
|
17
|
-
options.new(args).render_in(self, &block)
|
18
|
-
elsif options.is_a?(Hash) && options.has_key?(:component)
|
19
|
-
options[:component].new(options[:locals]).render_in(self, &block)
|
20
|
-
else
|
21
|
-
super
|
22
|
-
end
|
23
|
-
end
|
24
|
-
end
|
25
|
-
|
26
|
-
prepend RenderMonkeyPatch
|
27
|
-
end
|