actionview-component 1.5.2 → 1.7.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 30c2d62abc3a38ba7f383a934ea3a95d6e490f81868d062d9263130e6d694d84
4
- data.tar.gz: 8cc4375853920bd79183e9e634e00a9f4804726c2fc2a60328f3f1ad664bccef
3
+ metadata.gz: '085434585e1e3fa1493bbc7bd90439b1645015acd66a713a33545c0eb4677dc4'
4
+ data.tar.gz: 34ec8d8f390471689fbd1085c94885b74c4f9a1b0b7d9b78650dae506520dd13
5
5
  SHA512:
6
- metadata.gz: 98e2021c3308374c1ef1d92d044b18bdcbb071e422a52cbfe102f984cbf34521d6c19b4f9b60b511f3f9ffa1e3b2e0fc7b5ec31dcf22420f5d4240baad42e881
7
- data.tar.gz: 7e68770c3ec42381c7fce1028834b95ce4c6d04e1ce71e2849ce04a99b73b3539c2246c35bd77421a63c754e66fac083adaac1ae531bc37fb306293cd7a9a61e
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
@@ -10,6 +10,7 @@
10
10
  /test/version_tmp/
11
11
  /tmp/
12
12
  /test/log/*
13
+ /test/app/tmp/*
13
14
 
14
15
  # Used by dotenv library to load environment variables.
15
16
  # .env
@@ -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.
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- actionview-component (1.5.2)
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.7)
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 `6.1`, this gem will then exist to serve as a backport.
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/test_helpers"
375
+ require "action_view/component/test_case"
190
376
 
191
- class MyComponentTest < Minitest::Test
192
- include ActionView::Component::TestHelpers
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
- def test_render_component_for_tablet
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`s provide a way to see how components look by visiting a special URL that renders them.
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 "action_view/component/monkey_patch"
4
- require "action_view/component/base"
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::Previews
10
+ include ActionView::Component::Previewable
14
11
 
15
12
  delegate :form_authenticity_token, :protect_against_forgery?, to: :helpers
16
13
 
17
- validate :variant_exists
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
- private
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
- def variant_exists
98
- return if self.class.variants.include?(@variant) || @variant.nil?
99
+ if block_given?
100
+ content = view_context.capture(&block)
101
+ end
99
102
 
100
- errors.add(:variant, "'#{@variant}' has no template defined")
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
- # Require #initialize to be defined so that we can use
126
- # method#source_location to look up the file name
127
- # of the component.
128
- #
129
- # If we were able to only support Ruby 2.7+,
130
- # We could just use Module#const_source_location,
131
- # rendering this unnecessary.
132
- raise NotImplementedError.new("#{self} must implement #initialize.") unless self.instance_method(:initialize).owner == self
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
- instance_method(:initialize).source_location[0]
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 @compiled && ActionView::Base.cache_template_loading
157
+ def compile(validate: false)
158
+ return if compiled?
142
159
 
143
- validate_templates
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
- (Dir["#{source_location.sub(/#{File.extname(source_location)}$/, '')}.*{#{ActionView::Template.template_handler_extensions.join(',')}}"] - [source_location]).each_with_object([]) do |path, memo|
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: path.split(".").second.split("+")[1]&.to_sym,
178
- handler: path.split(".").last
212
+ variant: pieces.second.split("+").second&.to_sym,
213
+ handler: pieces.last
179
214
  }
180
215
  end
181
216
  end
182
217
 
183
- def validate_templates
184
- if templates.empty?
185
- raise NotImplementedError.new("Could not find a template file for #{self}.")
186
- end
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
- if templates.select { |template| template[:variant].nil? }.length > 1
189
- raise StandardError.new("More than one template found for #{self}. There can only be one default template file per component.")
190
- end
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
- variants.each_with_object(Hash.new(0)) { |variant, counts| counts[variant] += 1 }.each do |variant, count|
193
- next unless count > 1
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
- raise StandardError.new("More than one template found for variant '#{variant}' in #{self}. There can only be one template file per variant.")
196
- end
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)
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionView
4
+ module Component # :nodoc:
5
+ module Conversion
6
+ def to_component_class
7
+ "#{self.class.name}Component".safe_constantize
8
+ end
9
+ end
10
+ end
11
+ end
@@ -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 #:nodoc:
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: @layout || "layouts/application",
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 "railties/lib/rails/components_controller"
4
- require "railties/lib/rails/component_examples_controller"
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
- # Disabled because `ActionView::Component::Base` doesn't implement `#action_methods`
45
- # initializer "action_view_component.eager_load_actions" do
46
- # ActiveSupport.on_load(:after_initialize) do
47
- # ActionView::Component::Base.descendants.each(&:action_methods) if config.eager_load
48
- # end
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
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionView
4
+ module Component
5
+ class TemplateError < StandardError
6
+ def initialize(errors)
7
+ super(errors.join(", "))
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/test_case"
4
+
5
+ module ActionView
6
+ module Component
7
+ class TestCase < ActiveSupport::TestCase
8
+ include ActionView::Component::TestHelpers
9
+ end
10
+ end
11
+ 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)).css("body > *")
7
+ Nokogiri::HTML.fragment(controller.view_context.render(component, args, &block))
8
8
  end
9
9
 
10
10
  def controller
@@ -4,8 +4,8 @@ module ActionView
4
4
  module Component
5
5
  module VERSION
6
6
  MAJOR = 1
7
- MINOR = 5
8
- PATCH = 2
7
+ MINOR = 7
8
+ PATCH = 0
9
9
 
10
10
  STRING = [MAJOR, MINOR, PATCH].join(".")
11
11
  end
@@ -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 < ActiveSupport::TestCase
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 %>, attr: "value") { "Hello, components!" }.css("span").to_html
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.5.2
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: 2019-11-22 00:00:00.000000000 Z
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/monkey_patch.rb
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