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