actionview-component 1.6.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: 57dd1d0375f05f3a5849670716be000a0c8074f4ae63b1d422278006b07e5b32
4
- data.tar.gz: 42309cc4774f3405e338cd48a84bc6ee6619ffa3306d98ab8432e8a196d6cb5f
3
+ metadata.gz: '085434585e1e3fa1493bbc7bd90439b1645015acd66a713a33545c0eb4677dc4'
4
+ data.tar.gz: 34ec8d8f390471689fbd1085c94885b74c4f9a1b0b7d9b78650dae506520dd13
5
5
  SHA512:
6
- metadata.gz: fdaad351b7db8bb6aab8e94294c221c50993cdd851fac8d43f64c3628627b5652be8fd3bb93f450467f74401433d4d3fffea560ca9ddc8c78fe645fbe0596056
7
- data.tar.gz: f75561cba609fd4eecf3ac958a0cf9ef9ee9300772c3016749cf3ab5bea5e5ac5f4c2005f47623587e5a80fb9c9d71a18aeaad33fc9d89ba8c1123191dc0345b
6
+ metadata.gz: 89e3c7d8b6c1d67a29f2ad67a032e424c42f57dd719d5b8cbde01b167385d9d2aeb5feb8fd77fcd9284e0a33da828b935edeebbb758c7cd68951178eea2d3b14
7
+ data.tar.gz: bc76f28233c72f7d5ff3a818429e8c763649db3ec46e406b554070cd456b73ae923905ecb94d3b37949ca5c6b0960890594a341e72a9755343462cf4c5c24cff
@@ -1,3 +1,13 @@
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
+
1
11
  # v1.6.2
2
12
 
3
13
  * Fix Uninitialized Constant error.
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- actionview-component (1.6.2)
4
+ actionview-component (1.7.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
data/README.md CHANGED
@@ -199,6 +199,174 @@ An error will be raised:
199
199
 
200
200
  `ActiveModel::ValidationError: Validation failed: Title can't be blank`
201
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
+
202
370
  ### Testing
203
371
 
204
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:
@@ -16,6 +16,7 @@ module ActionView
16
16
  autoload :TestHelpers
17
17
  autoload :TestCase
18
18
  autoload :RenderMonkeyPatch
19
+ autoload :TemplateError
19
20
  end
20
21
  end
21
22
 
@@ -11,6 +11,9 @@ module ActionView
11
11
 
12
12
  delegate :form_authenticity_token, :protect_against_forgery?, to: :helpers
13
13
 
14
+ class_attribute :content_areas, default: []
15
+ self.content_areas = [] # default doesn't work until Rails 5.2
16
+
14
17
  # Entrypoint for rendering components. Called by ActionView::Base#render.
15
18
  #
16
19
  # view_context: ActionView context from calling view
@@ -37,7 +40,7 @@ module ActionView
37
40
  # <span title="greeting">Hello, world!</span>
38
41
  #
39
42
  def render_in(view_context, *args, &block)
40
- self.class.compile
43
+ self.class.compile!
41
44
  @view_context = view_context
42
45
  @view_renderer ||= view_context.view_renderer
43
46
  @lookup_context ||= view_context.lookup_context
@@ -47,7 +50,8 @@ module ActionView
47
50
  old_current_template = @current_template
48
51
  @current_template = self
49
52
 
50
- @content = view_context.capture(&block) if block_given?
53
+ @content = view_context.capture(self, &block) if block_given?
54
+
51
55
  validate!
52
56
 
53
57
  send(self.class.call_method_name(@variant))
@@ -87,6 +91,19 @@ module ActionView
87
91
  @variant
88
92
  end
89
93
 
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
98
+
99
+ if block_given?
100
+ content = view_context.capture(&block)
101
+ end
102
+
103
+ instance_variable_set("@#{area}".to_sym, content)
104
+ nil
105
+ end
106
+
90
107
  private
91
108
 
92
109
  def request
@@ -110,34 +127,40 @@ module ActionView
110
127
  end
111
128
  end
112
129
 
113
- def has_initializer?
114
- self.instance_method(:initialize).owner == self
115
- end
116
-
117
130
  def source_location
118
- # Require #initialize to be defined so that we can use
119
- # method#source_location to look up the file name
120
- # of the component.
121
- #
122
- # If we were able to only support Ruby 2.7+,
123
- # We could just use Module#const_source_location,
124
- # rendering this unnecessary.
125
- raise NotImplementedError.new("#{self} must implement #initialize.") unless has_initializer?
126
-
127
- instance_method(:initialize).source_location[0]
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
128
144
  end
129
145
 
130
146
  def compiled?
131
147
  @compiled && ActionView::Base.cache_template_loading
132
148
  end
133
149
 
150
+ def compile!
151
+ compile(validate: true)
152
+ end
153
+
134
154
  # Compile templates to instance methods, assuming they haven't been compiled already.
135
155
  # We could in theory do this on app boot, at least in production environments.
136
156
  # Right now this just compiles the first time the component is rendered.
137
- def compile
157
+ def compile(validate: false)
138
158
  return if compiled?
139
159
 
140
- validate_templates
160
+ if template_errors.present?
161
+ raise ActionView::Component::TemplateError.new(template_errors) if validate
162
+ return false
163
+ end
141
164
 
142
165
  templates.each do |template|
143
166
  class_eval <<-RUBY, __FILE__, __LINE__ + 1
@@ -164,9 +187,18 @@ module ActionView
164
187
  source_location
165
188
  end
166
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
196
+ end
197
+
167
198
  private
168
199
 
169
200
  def matching_views_in_source_location
201
+ return [] unless source_location
170
202
  (Dir["#{source_location.sub(/#{File.extname(source_location)}$/, '')}.*{#{ActionView::Template.template_handler_extensions.join(',')}}"] - [source_location])
171
203
  end
172
204
 
@@ -177,26 +209,34 @@ module ActionView
177
209
 
178
210
  memo << {
179
211
  path: path,
180
- variant: pieces.second.split("+")[1]&.to_sym,
212
+ variant: pieces.second.split("+").second&.to_sym,
181
213
  handler: pieces.last
182
214
  }
183
215
  end
184
216
  end
185
217
 
186
- def validate_templates
187
- if templates.empty?
188
- raise NotImplementedError.new("Could not find a template file for #{self}.")
189
- 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?
190
224
 
191
- if templates.select { |template| template[:variant].nil? }.length > 1
192
- raise StandardError.new("More than one template found for #{self}. There can only be one default template file per component.")
193
- 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
194
228
 
195
- variants.each_with_object(Hash.new(0)) { |variant, counts| counts[variant] += 1 }.each do |variant, count|
196
- 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
197
234
 
198
- raise StandardError.new("More than one template found for variant '#{variant}' in #{self}. There can only be one template file per variant.")
199
- 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
200
240
  end
201
241
 
202
242
  def compiled_template(file_path)
@@ -35,9 +35,7 @@ module ActionView
35
35
 
36
36
  initializer "action_view_component.eager_load_actions" do
37
37
  ActiveSupport.on_load(:after_initialize) do
38
- ActionView::Component::Base.descendants.each do |descendant|
39
- descendant.compile if descendant.has_initializer? && config.eager_load
40
- end
38
+ ActionView::Component::Base.descendants.each(&:compile)
41
39
  end
42
40
  end
43
41
 
@@ -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
@@ -4,8 +4,8 @@ module ActionView
4
4
  module Component
5
5
  module VERSION
6
6
  MAJOR = 1
7
- MINOR = 6
8
- PATCH = 2
7
+ MINOR = 7
8
+ PATCH = 0
9
9
 
10
10
  STRING = [MAJOR, MINOR, PATCH].join(".")
11
11
  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.6.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: 2020-01-06 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
@@ -150,6 +150,7 @@ files:
150
150
  - lib/action_view/component/previewable.rb
151
151
  - lib/action_view/component/railtie.rb
152
152
  - lib/action_view/component/render_monkey_patch.rb
153
+ - lib/action_view/component/template_error.rb
153
154
  - lib/action_view/component/test_case.rb
154
155
  - lib/action_view/component/test_helpers.rb
155
156
  - lib/action_view/component/version.rb