actionview-component 1.6.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: 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