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 +4 -4
- data/CHANGELOG.md +10 -0
- data/Gemfile.lock +1 -1
- data/README.md +168 -0
- data/lib/action_view/component.rb +1 -0
- data/lib/action_view/component/base.rb +70 -30
- data/lib/action_view/component/railtie.rb +1 -3
- data/lib/action_view/component/template_error.rb +11 -0
- data/lib/action_view/component/version.rb +2 -2
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: '085434585e1e3fa1493bbc7bd90439b1645015acd66a713a33545c0eb4677dc4'
|
4
|
+
data.tar.gz: 34ec8d8f390471689fbd1085c94885b74c4f9a1b0b7d9b78650dae506520dd13
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 89e3c7d8b6c1d67a29f2ad67a032e424c42f57dd719d5b8cbde01b167385d9d2aeb5feb8fd77fcd9284e0a33da828b935edeebbb758c7cd68951178eea2d3b14
|
7
|
+
data.tar.gz: bc76f28233c72f7d5ff3a818429e8c763649db3ec46e406b554070cd456b73ae923905ecb94d3b37949ca5c6b0960890594a341e72a9755343462cf4c5c24cff
|
data/CHANGELOG.md
CHANGED
data/Gemfile.lock
CHANGED
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:
|
@@ -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
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
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
|
-
|
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("+")
|
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
|
187
|
-
|
188
|
-
|
189
|
-
|
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
|
-
|
192
|
-
|
193
|
-
|
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
|
-
|
196
|
-
|
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
|
-
|
199
|
-
|
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
|
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
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: actionview-component
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.7.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- GitHub Open Source
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-01-
|
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
|