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 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