view_component 2.5.1 → 2.10.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.
Potentially problematic release.
This version of view_component might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/CHANGELOG.md +38 -0
- data/README.md +317 -213
- data/app/controllers/{rails/view_components_controller.rb → view_components_controller.rb} +4 -11
- data/app/views/view_components/index.html.erb +8 -0
- data/app/views/view_components/preview.html.erb +1 -0
- data/{lib/railties/lib/rails/templates/rails/components → app/views/view_components}/previews.html.erb +1 -1
- data/lib/view_component/base.rb +136 -38
- data/lib/view_component/collection.rb +5 -3
- data/lib/view_component/compile_cache.rb +24 -0
- data/lib/view_component/engine.rb +7 -2
- data/lib/view_component/preview.rb +1 -0
- data/lib/view_component/previewable.rb +10 -0
- data/lib/view_component/test_helpers.rb +5 -3
- data/lib/view_component/version.rb +2 -2
- metadata +37 -9
- data/lib/railties/lib/rails.rb +0 -5
- data/lib/railties/lib/rails/templates/rails/components/index.html.erb +0 -8
- data/lib/railties/lib/rails/templates/rails/components/preview.html.erb +0 -1
@@ -2,8 +2,8 @@
|
|
2
2
|
|
3
3
|
require "rails/application_controller"
|
4
4
|
|
5
|
-
class
|
6
|
-
prepend_view_path File.expand_path("
|
5
|
+
class ViewComponentsController < Rails::ApplicationController # :nodoc:
|
6
|
+
prepend_view_path File.expand_path("../views", __dir__)
|
7
7
|
|
8
8
|
around_action :set_locale, only: :previews
|
9
9
|
before_action :find_preview, only: :previews
|
@@ -16,26 +16,19 @@ class Rails::ViewComponentsController < Rails::ApplicationController # :nodoc:
|
|
16
16
|
def index
|
17
17
|
@previews = ViewComponent::Preview.all
|
18
18
|
@page_title = "Component Previews"
|
19
|
-
# rubocop:disable GitHub/RailsControllerRenderPathsExist
|
20
|
-
render "components/index"
|
21
|
-
# rubocop:enable GitHub/RailsControllerRenderPathsExist
|
22
19
|
end
|
23
20
|
|
24
21
|
def previews
|
25
22
|
if params[:path] == @preview.preview_name
|
26
23
|
@page_title = "Component Previews for #{@preview.preview_name}"
|
27
|
-
|
28
|
-
render "components/previews"
|
29
|
-
# rubocop:enable GitHub/RailsControllerRenderPathsExist
|
24
|
+
render "view_components/previews"
|
30
25
|
else
|
31
26
|
prepend_application_view_paths
|
32
27
|
@example_name = File.basename(params[:path])
|
33
28
|
@render_args = @preview.render_args(@example_name, params: params.permit!)
|
34
29
|
layout = @render_args[:layout]
|
35
30
|
opts = layout.nil? ? {} : { layout: layout }
|
36
|
-
|
37
|
-
render "components/preview", **opts
|
38
|
-
# rubocop:enable GitHub/RailsControllerRenderPathsExist
|
31
|
+
render "view_components/preview", opts
|
39
32
|
end
|
40
33
|
end
|
41
34
|
|
@@ -0,0 +1,8 @@
|
|
1
|
+
<% @previews.each do |preview| %>
|
2
|
+
<h3><%= link_to preview.preview_name.titleize, preview_view_component_path(preview.preview_name) %></h3>
|
3
|
+
<ul>
|
4
|
+
<% preview.examples.each do |preview_example| %>
|
5
|
+
<li><%= link_to preview_example, preview_view_component_path("#{preview.preview_name}/#{preview_example}") %></li>
|
6
|
+
<% end %>
|
7
|
+
</ul>
|
8
|
+
<% end %>
|
@@ -0,0 +1 @@
|
|
1
|
+
<%= render(@render_args[:component], @render_args[:args], &@render_args[:block])%>
|
@@ -1,6 +1,6 @@
|
|
1
1
|
<h3><%= @preview.preview_name.titleize %></h3>
|
2
2
|
<ul>
|
3
3
|
<% @preview.examples.each do |example| %>
|
4
|
-
<li><%= link_to example, "
|
4
|
+
<li><%= link_to example, preview_view_component_path("#{@preview.preview_name}/#{example}") %></li>
|
5
5
|
<% end %>
|
6
6
|
</ul>
|
data/lib/view_component/base.rb
CHANGED
@@ -3,6 +3,7 @@
|
|
3
3
|
require "action_view"
|
4
4
|
require "active_support/configurable"
|
5
5
|
require "view_component/collection"
|
6
|
+
require "view_component/compile_cache"
|
6
7
|
require "view_component/previewable"
|
7
8
|
|
8
9
|
module ViewComponent
|
@@ -10,15 +11,11 @@ module ViewComponent
|
|
10
11
|
include ActiveSupport::Configurable
|
11
12
|
include ViewComponent::Previewable
|
12
13
|
|
14
|
+
# For CSRF authenticity tokens in forms
|
13
15
|
delegate :form_authenticity_token, :protect_against_forgery?, to: :helpers
|
14
16
|
|
15
|
-
class_attribute :content_areas
|
16
|
-
self.content_areas = [] # default doesn't work until Rails 5.2
|
17
|
-
|
18
|
-
# Render a component collection.
|
19
|
-
def self.with_collection(*args)
|
20
|
-
Collection.new(self, *args)
|
21
|
-
end
|
17
|
+
class_attribute :content_areas
|
18
|
+
self.content_areas = [] # class_attribute:default doesn't work until Rails 5.2
|
22
19
|
|
23
20
|
# Entrypoint for rendering components.
|
24
21
|
#
|
@@ -45,20 +42,32 @@ module ViewComponent
|
|
45
42
|
# <span title="greeting">Hello, world!</span>
|
46
43
|
#
|
47
44
|
def render_in(view_context, &block)
|
48
|
-
self.class.compile
|
45
|
+
self.class.compile(raise_errors: true)
|
46
|
+
|
49
47
|
@view_context = view_context
|
50
|
-
@view_renderer ||= view_context.view_renderer
|
51
48
|
@lookup_context ||= view_context.lookup_context
|
49
|
+
|
50
|
+
# required for path helpers in older Rails versions
|
51
|
+
@view_renderer ||= view_context.view_renderer
|
52
|
+
|
53
|
+
# For content_for
|
52
54
|
@view_flow ||= view_context.view_flow
|
55
|
+
|
56
|
+
# For i18n
|
53
57
|
@virtual_path ||= virtual_path
|
58
|
+
|
59
|
+
# For template variants (+phone, +desktop, etc.)
|
54
60
|
@variant = @lookup_context.variants.first
|
55
61
|
|
62
|
+
# For caching, such as #cache_if
|
63
|
+
@current_template = nil unless defined?(@current_template)
|
56
64
|
old_current_template = @current_template
|
57
65
|
@current_template = self
|
58
66
|
|
67
|
+
# Assign captured content passed to component as a block to @content
|
59
68
|
@content = view_context.capture(self, &block) if block_given?
|
60
69
|
|
61
|
-
|
70
|
+
before_render
|
62
71
|
|
63
72
|
if render?
|
64
73
|
send(self.class.call_method_name(@variant))
|
@@ -69,6 +78,10 @@ module ViewComponent
|
|
69
78
|
@current_template = old_current_template
|
70
79
|
end
|
71
80
|
|
81
|
+
def before_render
|
82
|
+
before_render_check
|
83
|
+
end
|
84
|
+
|
72
85
|
def before_render_check
|
73
86
|
# noop
|
74
87
|
end
|
@@ -77,12 +90,10 @@ module ViewComponent
|
|
77
90
|
true
|
78
91
|
end
|
79
92
|
|
80
|
-
def self.short_identifier
|
81
|
-
@short_identifier ||= defined?(Rails.root) ? source_location.sub("#{Rails.root}/", "") : source_location
|
82
|
-
end
|
83
|
-
|
84
93
|
def initialize(*); end
|
85
94
|
|
95
|
+
# If trying to render a partial or template inside a component,
|
96
|
+
# pass the render call to the parent view_context.
|
86
97
|
def render(options = {}, args = {}, &block)
|
87
98
|
if options.is_a?(String) || (options.is_a?(Hash) && options.has_key?(:partial))
|
88
99
|
view_context.render(options, args, &block)
|
@@ -95,7 +106,7 @@ module ViewComponent
|
|
95
106
|
@controller ||= view_context.controller
|
96
107
|
end
|
97
108
|
|
98
|
-
# Provides a proxy to access helper methods
|
109
|
+
# Provides a proxy to access helper methods
|
99
110
|
def helpers
|
100
111
|
@helpers ||= view_context
|
101
112
|
end
|
@@ -105,14 +116,17 @@ module ViewComponent
|
|
105
116
|
self.class.source_location.gsub(%r{(.*app/components)|(\.rb)}, "")
|
106
117
|
end
|
107
118
|
|
119
|
+
# For caching, such as #cache_if
|
108
120
|
def view_cache_dependencies
|
109
121
|
[]
|
110
122
|
end
|
111
123
|
|
112
|
-
|
124
|
+
# For caching, such as #cache_if
|
125
|
+
def format
|
113
126
|
@variant
|
114
127
|
end
|
115
128
|
|
129
|
+
# Assign the provided content to the content area accessor
|
116
130
|
def with(area, content = nil, &block)
|
117
131
|
unless content_areas.include?(area)
|
118
132
|
raise ArgumentError.new "Unknown content_area '#{area}' - expected one of '#{content_areas}'"
|
@@ -128,6 +142,9 @@ module ViewComponent
|
|
128
142
|
|
129
143
|
private
|
130
144
|
|
145
|
+
# Exposes the current request to the component.
|
146
|
+
# Use sparingly as doing so introduces coupling
|
147
|
+
# that inhibits encapsulation & reuse.
|
131
148
|
def request
|
132
149
|
@request ||= controller.request
|
133
150
|
end
|
@@ -143,7 +160,18 @@ module ViewComponent
|
|
143
160
|
class << self
|
144
161
|
attr_accessor :source_location
|
145
162
|
|
163
|
+
# Render a component collection.
|
164
|
+
def with_collection(*args)
|
165
|
+
Collection.new(self, *args)
|
166
|
+
end
|
167
|
+
|
168
|
+
# Provide identifier for ActionView template annotations
|
169
|
+
def short_identifier
|
170
|
+
@short_identifier ||= defined?(Rails.root) ? source_location.sub("#{Rails.root}/", "") : source_location
|
171
|
+
end
|
172
|
+
|
146
173
|
def inherited(child)
|
174
|
+
# If we're in Rails, add application url_helpers to the component context
|
147
175
|
if defined?(Rails)
|
148
176
|
child.include Rails.application.routes.url_helpers unless child < Rails.application.routes.url_helpers
|
149
177
|
end
|
@@ -165,58 +193,83 @@ module ViewComponent
|
|
165
193
|
end
|
166
194
|
|
167
195
|
def compiled?
|
168
|
-
|
169
|
-
end
|
170
|
-
|
171
|
-
def compile!
|
172
|
-
compile(raise_template_errors: true)
|
196
|
+
CompileCache.compiled?(self)
|
173
197
|
end
|
174
198
|
|
175
199
|
# Compile templates to instance methods, assuming they haven't been compiled already.
|
176
|
-
#
|
177
|
-
#
|
178
|
-
|
200
|
+
#
|
201
|
+
# Do as much work as possible in this step, as doing so reduces the amount
|
202
|
+
# of work done each time a component is rendered.
|
203
|
+
def compile(raise_errors: false)
|
179
204
|
return if compiled?
|
180
205
|
|
181
206
|
if template_errors.present?
|
182
|
-
raise ViewComponent::TemplateError.new(template_errors) if
|
207
|
+
raise ViewComponent::TemplateError.new(template_errors) if raise_errors
|
183
208
|
return false
|
184
209
|
end
|
185
210
|
|
211
|
+
if instance_methods(false).include?(:before_render_check)
|
212
|
+
ActiveSupport::Deprecation.warn(
|
213
|
+
"`before_render_check` will be removed in v3.0.0. Use `before_render` instead."
|
214
|
+
)
|
215
|
+
end
|
216
|
+
|
217
|
+
# Remove any existing singleton methods,
|
218
|
+
# as Ruby warns when redefining a method.
|
219
|
+
remove_possible_singleton_method(:variants)
|
220
|
+
remove_possible_singleton_method(:collection_parameter)
|
221
|
+
remove_possible_singleton_method(:collection_counter_parameter)
|
222
|
+
remove_possible_singleton_method(:counter_argument_present?)
|
223
|
+
|
186
224
|
define_singleton_method(:variants) do
|
187
225
|
templates.map { |template| template[:variant] } + variants_from_inline_calls(inline_calls)
|
188
226
|
end
|
189
227
|
|
190
|
-
define_singleton_method(:
|
191
|
-
|
228
|
+
define_singleton_method(:collection_parameter) do
|
229
|
+
if provided_collection_parameter
|
230
|
+
provided_collection_parameter
|
231
|
+
else
|
232
|
+
name.demodulize.underscore.chomp("_component").to_sym
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
define_singleton_method(:collection_counter_parameter) do
|
237
|
+
"#{collection_parameter}_counter".to_sym
|
192
238
|
end
|
193
239
|
|
194
240
|
define_singleton_method(:counter_argument_present?) do
|
195
|
-
instance_method(:initialize).parameters.map(&:second).include?(
|
241
|
+
instance_method(:initialize).parameters.map(&:second).include?(collection_counter_parameter)
|
196
242
|
end
|
197
243
|
|
244
|
+
validate_collection_parameter! if raise_errors
|
245
|
+
|
198
246
|
# If template name annotations are turned on, a line is dynamically
|
199
247
|
# added with a comment. In this case, we want to return a different
|
200
248
|
# starting line number so errors that are raised will point to the
|
201
249
|
# correct line in the component template.
|
202
250
|
line_number =
|
203
|
-
if ActionView::Base.respond_to?(:
|
204
|
-
ActionView::Base.
|
251
|
+
if ActionView::Base.respond_to?(:annotate_rendered_view_with_filenames) &&
|
252
|
+
ActionView::Base.annotate_rendered_view_with_filenames
|
205
253
|
-2
|
206
254
|
else
|
207
255
|
-1
|
208
256
|
end
|
209
257
|
|
210
258
|
templates.each do |template|
|
259
|
+
# Remove existing compiled template methods,
|
260
|
+
# as Ruby warns when redefining a method.
|
261
|
+
method_name = call_method_name(template[:variant])
|
262
|
+
undef_method(method_name.to_sym) if instance_methods.include?(method_name.to_sym)
|
263
|
+
|
211
264
|
class_eval <<-RUBY, template[:path], line_number
|
212
|
-
def #{
|
265
|
+
def #{method_name}
|
213
266
|
@output_buffer = ActionView::OutputBuffer.new
|
214
267
|
#{compiled_template(template[:path])}
|
215
268
|
end
|
216
269
|
RUBY
|
217
270
|
end
|
218
271
|
|
219
|
-
|
272
|
+
CompileCache.register self
|
220
273
|
end
|
221
274
|
|
222
275
|
# we'll eventually want to update this to support other types
|
@@ -236,21 +289,51 @@ module ViewComponent
|
|
236
289
|
if areas.include?(:content)
|
237
290
|
raise ArgumentError.new ":content is a reserved content area name. Please use another name, such as ':body'"
|
238
291
|
end
|
239
|
-
attr_reader
|
292
|
+
attr_reader(*areas)
|
240
293
|
self.content_areas = areas
|
241
294
|
end
|
242
295
|
|
243
|
-
# Support overriding
|
296
|
+
# Support overriding collection parameter name
|
244
297
|
def with_collection_parameter(param)
|
245
|
-
@
|
298
|
+
@provided_collection_parameter = param
|
246
299
|
end
|
247
300
|
|
248
|
-
|
249
|
-
|
301
|
+
# Ensure the component initializer accepts the
|
302
|
+
# collection parameter. By default, we do not
|
303
|
+
# validate that the default parameter name
|
304
|
+
# is accepted, as support for collection
|
305
|
+
# rendering is optional.
|
306
|
+
def validate_collection_parameter!(validate_default: false)
|
307
|
+
parameter = validate_default ? collection_parameter : provided_collection_parameter
|
308
|
+
|
309
|
+
return unless parameter
|
310
|
+
return if initialize_parameters.map(&:last).include?(parameter)
|
311
|
+
|
312
|
+
# If Ruby cannot parse the component class, then the initalize
|
313
|
+
# parameters will be empty and ViewComponent will not be able to render
|
314
|
+
# the component.
|
315
|
+
if initialize_parameters.empty?
|
316
|
+
raise ArgumentError.new(
|
317
|
+
"#{self} initializer is empty or invalid."
|
318
|
+
)
|
319
|
+
end
|
320
|
+
|
321
|
+
raise ArgumentError.new(
|
322
|
+
"#{self} initializer must accept " \
|
323
|
+
"`#{parameter}` collection parameter."
|
324
|
+
)
|
250
325
|
end
|
251
326
|
|
252
327
|
private
|
253
328
|
|
329
|
+
def initialize_parameters
|
330
|
+
instance_method(:initialize).parameters
|
331
|
+
end
|
332
|
+
|
333
|
+
def provided_collection_parameter
|
334
|
+
@provided_collection_parameter ||= nil
|
335
|
+
end
|
336
|
+
|
254
337
|
def compiled_template(file_path)
|
255
338
|
handler = ActionView::Template.handler_for_extension(File.extname(file_path).gsub(".", ""))
|
256
339
|
template = File.read(file_path)
|
@@ -280,7 +363,22 @@ module ViewComponent
|
|
280
363
|
|
281
364
|
def matching_views_in_source_location
|
282
365
|
return [] unless source_location
|
283
|
-
|
366
|
+
|
367
|
+
location_without_extension = source_location.chomp(File.extname(source_location))
|
368
|
+
|
369
|
+
extenstions = ActionView::Template.template_handler_extensions.join(",")
|
370
|
+
|
371
|
+
# view files in the same directory as te component
|
372
|
+
sidecar_files = Dir["#{location_without_extension}.*{#{extenstions}}"]
|
373
|
+
|
374
|
+
# view files in a directory named like the component
|
375
|
+
directory = File.dirname(source_location)
|
376
|
+
filename = File.basename(source_location, ".rb")
|
377
|
+
component_name = name.demodulize.underscore
|
378
|
+
|
379
|
+
sidecar_directory_files = Dir["#{directory}/#{component_name}/#{filename}.*{#{extenstions}}"]
|
380
|
+
|
381
|
+
(sidecar_files - [source_location] + sidecar_directory_files)
|
284
382
|
end
|
285
383
|
|
286
384
|
def templates
|
@@ -5,7 +5,9 @@ module ViewComponent
|
|
5
5
|
def render_in(view_context, &block)
|
6
6
|
iterator = ActionView::PartialIteration.new(@collection.size)
|
7
7
|
|
8
|
-
@component.compile
|
8
|
+
@component.compile(raise_errors: true)
|
9
|
+
@component.validate_collection_parameter!(validate_default: true)
|
10
|
+
|
9
11
|
@collection.map do |item|
|
10
12
|
content = @component.new(component_options(item, iterator)).render_in(view_context, &block)
|
11
13
|
iterator.iterate!
|
@@ -30,8 +32,8 @@ module ViewComponent
|
|
30
32
|
end
|
31
33
|
|
32
34
|
def component_options(item, iterator)
|
33
|
-
item_options = { @component.
|
34
|
-
item_options[@component.
|
35
|
+
item_options = { @component.collection_parameter => item }
|
36
|
+
item_options[@component.collection_counter_parameter] = iterator.index + 1 if @component.counter_argument_present?
|
35
37
|
|
36
38
|
@options.merge(item_options)
|
37
39
|
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ViewComponent
|
4
|
+
# Keeps track of which templates have already been compiled
|
5
|
+
# This is not part of the public API
|
6
|
+
module CompileCache
|
7
|
+
mattr_accessor :cache, instance_reader: false, instance_accessor: false do
|
8
|
+
Set.new
|
9
|
+
end
|
10
|
+
module_function
|
11
|
+
|
12
|
+
def register(klass)
|
13
|
+
cache << klass
|
14
|
+
end
|
15
|
+
|
16
|
+
def compiled?(klass)
|
17
|
+
cache.include? klass
|
18
|
+
end
|
19
|
+
|
20
|
+
def invalidate!
|
21
|
+
cache.clear
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -11,6 +11,7 @@ module ViewComponent
|
|
11
11
|
options = app.config.view_component
|
12
12
|
|
13
13
|
options.show_previews = Rails.env.development? if options.show_previews.nil?
|
14
|
+
options.preview_route ||= ViewComponent::Base.preview_route
|
14
15
|
|
15
16
|
if options.show_previews
|
16
17
|
options.preview_path ||= defined?(Rails.root) ? "#{Rails.root}/test/components/previews" : nil
|
@@ -64,10 +65,14 @@ module ViewComponent
|
|
64
65
|
|
65
66
|
if options.show_previews
|
66
67
|
app.routes.prepend do
|
67
|
-
get
|
68
|
-
get "
|
68
|
+
get options.preview_route, to: "view_components#index", as: :preview_view_components, internal: true
|
69
|
+
get "#{options.preview_route}/*path", to: "view_components#previews", as: :preview_view_component, internal: true
|
69
70
|
end
|
70
71
|
end
|
72
|
+
|
73
|
+
app.executor.to_run :before do
|
74
|
+
CompileCache.invalidate! unless ActionView::Base.cache_template_loading
|
75
|
+
end
|
71
76
|
end
|
72
77
|
end
|
73
78
|
end
|