view_component 2.4.0 → 2.8.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 +42 -0
- data/README.md +336 -206
- data/app/controllers/{rails/view_components_controller.rb → view_components_controller.rb} +5 -12
- 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 +127 -33
- data/lib/view_component/collection.rb +14 -3
- data/lib/view_component/engine.rb +3 -2
- data/lib/view_component/preview.rb +6 -2
- data/lib/view_component/previewable.rb +10 -0
- data/lib/view_component/test_helpers.rb +5 -3
- data/lib/view_component/version.rb +1 -1
- metadata +36 -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
|
-
@render_args = @preview.render_args(@example_name)
|
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
@@ -10,15 +10,11 @@ module ViewComponent
|
|
10
10
|
include ActiveSupport::Configurable
|
11
11
|
include ViewComponent::Previewable
|
12
12
|
|
13
|
+
# For CSRF authenticity tokens in forms
|
13
14
|
delegate :form_authenticity_token, :protect_against_forgery?, to: :helpers
|
14
15
|
|
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
|
16
|
+
class_attribute :content_areas
|
17
|
+
self.content_areas = [] # class_attribute:default doesn't work until Rails 5.2
|
22
18
|
|
23
19
|
# Entrypoint for rendering components.
|
24
20
|
#
|
@@ -45,20 +41,32 @@ module ViewComponent
|
|
45
41
|
# <span title="greeting">Hello, world!</span>
|
46
42
|
#
|
47
43
|
def render_in(view_context, &block)
|
48
|
-
self.class.compile
|
44
|
+
self.class.compile(raise_errors: true)
|
45
|
+
|
49
46
|
@view_context = view_context
|
50
|
-
@view_renderer ||= view_context.view_renderer
|
51
47
|
@lookup_context ||= view_context.lookup_context
|
48
|
+
|
49
|
+
# required for path helpers in older Rails versions
|
50
|
+
@view_renderer ||= view_context.view_renderer
|
51
|
+
|
52
|
+
# For content_for
|
52
53
|
@view_flow ||= view_context.view_flow
|
54
|
+
|
55
|
+
# For i18n
|
53
56
|
@virtual_path ||= virtual_path
|
57
|
+
|
58
|
+
# For template variants (+phone, +desktop, etc.)
|
54
59
|
@variant = @lookup_context.variants.first
|
55
60
|
|
61
|
+
# For caching, such as #cache_if
|
62
|
+
@current_template = nil unless defined?(@current_template)
|
56
63
|
old_current_template = @current_template
|
57
64
|
@current_template = self
|
58
65
|
|
66
|
+
# Assign captured content passed to component as a block to @content
|
59
67
|
@content = view_context.capture(self, &block) if block_given?
|
60
68
|
|
61
|
-
|
69
|
+
before_render
|
62
70
|
|
63
71
|
if render?
|
64
72
|
send(self.class.call_method_name(@variant))
|
@@ -69,6 +77,10 @@ module ViewComponent
|
|
69
77
|
@current_template = old_current_template
|
70
78
|
end
|
71
79
|
|
80
|
+
def before_render
|
81
|
+
before_render_check
|
82
|
+
end
|
83
|
+
|
72
84
|
def before_render_check
|
73
85
|
# noop
|
74
86
|
end
|
@@ -77,12 +89,10 @@ module ViewComponent
|
|
77
89
|
true
|
78
90
|
end
|
79
91
|
|
80
|
-
def self.short_identifier
|
81
|
-
@short_identifier ||= defined?(Rails.root) ? source_location.sub("#{Rails.root}/", "") : source_location
|
82
|
-
end
|
83
|
-
|
84
92
|
def initialize(*); end
|
85
93
|
|
94
|
+
# If trying to render a partial or template inside a component,
|
95
|
+
# pass the render call to the parent view_context.
|
86
96
|
def render(options = {}, args = {}, &block)
|
87
97
|
if options.is_a?(String) || (options.is_a?(Hash) && options.has_key?(:partial))
|
88
98
|
view_context.render(options, args, &block)
|
@@ -95,7 +105,7 @@ module ViewComponent
|
|
95
105
|
@controller ||= view_context.controller
|
96
106
|
end
|
97
107
|
|
98
|
-
# Provides a proxy to access helper methods
|
108
|
+
# Provides a proxy to access helper methods
|
99
109
|
def helpers
|
100
110
|
@helpers ||= view_context
|
101
111
|
end
|
@@ -105,14 +115,17 @@ module ViewComponent
|
|
105
115
|
self.class.source_location.gsub(%r{(.*app/components)|(\.rb)}, "")
|
106
116
|
end
|
107
117
|
|
118
|
+
# For caching, such as #cache_if
|
108
119
|
def view_cache_dependencies
|
109
120
|
[]
|
110
121
|
end
|
111
122
|
|
112
|
-
|
123
|
+
# For caching, such as #cache_if
|
124
|
+
def format
|
113
125
|
@variant
|
114
126
|
end
|
115
127
|
|
128
|
+
# Assign the provided content to the content area accessor
|
116
129
|
def with(area, content = nil, &block)
|
117
130
|
unless content_areas.include?(area)
|
118
131
|
raise ArgumentError.new "Unknown content_area '#{area}' - expected one of '#{content_areas}'"
|
@@ -128,6 +141,9 @@ module ViewComponent
|
|
128
141
|
|
129
142
|
private
|
130
143
|
|
144
|
+
# Exposes the current request to the component.
|
145
|
+
# Use sparingly as doing so introduces coupling
|
146
|
+
# that inhibits encapsulation & reuse.
|
131
147
|
def request
|
132
148
|
@request ||= controller.request
|
133
149
|
end
|
@@ -143,7 +159,18 @@ module ViewComponent
|
|
143
159
|
class << self
|
144
160
|
attr_accessor :source_location
|
145
161
|
|
162
|
+
# Render a component collection.
|
163
|
+
def with_collection(*args)
|
164
|
+
Collection.new(self, *args)
|
165
|
+
end
|
166
|
+
|
167
|
+
# Provide identifier for ActionView template annotations
|
168
|
+
def short_identifier
|
169
|
+
@short_identifier ||= defined?(Rails.root) ? source_location.sub("#{Rails.root}/", "") : source_location
|
170
|
+
end
|
171
|
+
|
146
172
|
def inherited(child)
|
173
|
+
# If we're in Rails, add application url_helpers to the component context
|
147
174
|
if defined?(Rails)
|
148
175
|
child.include Rails.application.routes.url_helpers unless child < Rails.application.routes.url_helpers
|
149
176
|
end
|
@@ -165,43 +192,78 @@ module ViewComponent
|
|
165
192
|
end
|
166
193
|
|
167
194
|
def compiled?
|
168
|
-
@compiled
|
169
|
-
end
|
195
|
+
@compiled ||= false
|
170
196
|
|
171
|
-
|
172
|
-
compile(raise_template_errors: true)
|
197
|
+
@compiled && ActionView::Base.cache_template_loading
|
173
198
|
end
|
174
199
|
|
175
200
|
# Compile templates to instance methods, assuming they haven't been compiled already.
|
176
|
-
#
|
177
|
-
#
|
178
|
-
|
201
|
+
#
|
202
|
+
# Do as much work as possible in this step, as doing so reduces the amount
|
203
|
+
# of work done each time a component is rendered.
|
204
|
+
def compile(raise_errors: false)
|
179
205
|
return if compiled?
|
180
206
|
|
181
207
|
if template_errors.present?
|
182
|
-
raise ViewComponent::TemplateError.new(template_errors) if
|
208
|
+
raise ViewComponent::TemplateError.new(template_errors) if raise_errors
|
183
209
|
return false
|
184
210
|
end
|
185
211
|
|
212
|
+
if instance_methods(false).include?(:before_render_check)
|
213
|
+
ActiveSupport::Deprecation.warn(
|
214
|
+
"`before_render_check` will be removed in v3.0.0. Use `before_render` instead."
|
215
|
+
)
|
216
|
+
end
|
217
|
+
|
218
|
+
# Remove any existing singleton methods,
|
219
|
+
# as Ruby warns when redefining a method.
|
220
|
+
remove_possible_singleton_method(:variants)
|
221
|
+
remove_possible_singleton_method(:collection_parameter)
|
222
|
+
remove_possible_singleton_method(:collection_counter_parameter)
|
223
|
+
remove_possible_singleton_method(:counter_argument_present?)
|
224
|
+
|
186
225
|
define_singleton_method(:variants) do
|
187
226
|
templates.map { |template| template[:variant] } + variants_from_inline_calls(inline_calls)
|
188
227
|
end
|
189
228
|
|
229
|
+
define_singleton_method(:collection_parameter) do
|
230
|
+
if provided_collection_parameter
|
231
|
+
provided_collection_parameter
|
232
|
+
else
|
233
|
+
name.demodulize.underscore.chomp("_component").to_sym
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
define_singleton_method(:collection_counter_parameter) do
|
238
|
+
"#{collection_parameter}_counter".to_sym
|
239
|
+
end
|
240
|
+
|
241
|
+
define_singleton_method(:counter_argument_present?) do
|
242
|
+
instance_method(:initialize).parameters.map(&:second).include?(collection_counter_parameter)
|
243
|
+
end
|
244
|
+
|
245
|
+
validate_collection_parameter! if raise_errors
|
246
|
+
|
190
247
|
# If template name annotations are turned on, a line is dynamically
|
191
248
|
# added with a comment. In this case, we want to return a different
|
192
249
|
# starting line number so errors that are raised will point to the
|
193
250
|
# correct line in the component template.
|
194
251
|
line_number =
|
195
|
-
if ActionView::Base.respond_to?(:
|
196
|
-
ActionView::Base.
|
252
|
+
if ActionView::Base.respond_to?(:annotate_rendered_view_with_filenames) &&
|
253
|
+
ActionView::Base.annotate_rendered_view_with_filenames
|
197
254
|
-2
|
198
255
|
else
|
199
256
|
-1
|
200
257
|
end
|
201
258
|
|
202
259
|
templates.each do |template|
|
260
|
+
# Remove existing compiled template methods,
|
261
|
+
# as Ruby warns when redefining a method.
|
262
|
+
method_name = call_method_name(template[:variant])
|
263
|
+
undef_method(method_name.to_sym) if instance_methods.include?(method_name.to_sym)
|
264
|
+
|
203
265
|
class_eval <<-RUBY, template[:path], line_number
|
204
|
-
def #{
|
266
|
+
def #{method_name}
|
205
267
|
@output_buffer = ActionView::OutputBuffer.new
|
206
268
|
#{compiled_template(template[:path])}
|
207
269
|
end
|
@@ -228,21 +290,38 @@ module ViewComponent
|
|
228
290
|
if areas.include?(:content)
|
229
291
|
raise ArgumentError.new ":content is a reserved content area name. Please use another name, such as ':body'"
|
230
292
|
end
|
231
|
-
attr_reader
|
293
|
+
attr_reader(*areas)
|
232
294
|
self.content_areas = areas
|
233
295
|
end
|
234
296
|
|
235
|
-
# Support overriding
|
297
|
+
# Support overriding collection parameter name
|
236
298
|
def with_collection_parameter(param)
|
237
|
-
@
|
299
|
+
@provided_collection_parameter = param
|
238
300
|
end
|
239
301
|
|
240
|
-
|
241
|
-
|
302
|
+
# Ensure the component initializer accepts the
|
303
|
+
# collection parameter. By default, we do not
|
304
|
+
# validate that the default parameter name
|
305
|
+
# is accepted, as support for collection
|
306
|
+
# rendering is optional.
|
307
|
+
def validate_collection_parameter!(validate_default: false)
|
308
|
+
parameter = validate_default ? collection_parameter : provided_collection_parameter
|
309
|
+
|
310
|
+
return unless parameter
|
311
|
+
return if instance_method(:initialize).parameters.map(&:last).include?(parameter)
|
312
|
+
|
313
|
+
raise ArgumentError.new(
|
314
|
+
"#{self} initializer must accept " \
|
315
|
+
"`#{parameter}` collection parameter."
|
316
|
+
)
|
242
317
|
end
|
243
318
|
|
244
319
|
private
|
245
320
|
|
321
|
+
def provided_collection_parameter
|
322
|
+
@provided_collection_parameter ||= nil
|
323
|
+
end
|
324
|
+
|
246
325
|
def compiled_template(file_path)
|
247
326
|
handler = ActionView::Template.handler_for_extension(File.extname(file_path).gsub(".", ""))
|
248
327
|
template = File.read(file_path)
|
@@ -272,7 +351,22 @@ module ViewComponent
|
|
272
351
|
|
273
352
|
def matching_views_in_source_location
|
274
353
|
return [] unless source_location
|
275
|
-
|
354
|
+
|
355
|
+
location_without_extension = source_location.chomp(File.extname(source_location))
|
356
|
+
|
357
|
+
extenstions = ActionView::Template.template_handler_extensions.join(",")
|
358
|
+
|
359
|
+
# view files in the same directory as te component
|
360
|
+
sidecar_files = Dir["#{location_without_extension}.*{#{extenstions}}"]
|
361
|
+
|
362
|
+
# view files in a directory named like the component
|
363
|
+
directory = File.dirname(source_location)
|
364
|
+
filename = File.basename(source_location, ".rb")
|
365
|
+
component_name = name.demodulize.underscore
|
366
|
+
|
367
|
+
sidecar_directory_files = Dir["#{directory}/#{component_name}/#{filename}.*{#{extenstions}}"]
|
368
|
+
|
369
|
+
(sidecar_files - [source_location] + sidecar_directory_files)
|
276
370
|
end
|
277
371
|
|
278
372
|
def templates
|
@@ -1,13 +1,17 @@
|
|
1
|
-
|
2
1
|
# frozen_string_literal: true
|
3
2
|
|
4
3
|
module ViewComponent
|
5
4
|
class Collection
|
6
5
|
def render_in(view_context, &block)
|
7
|
-
|
6
|
+
iterator = ActionView::PartialIteration.new(@collection.size)
|
7
|
+
|
8
|
+
@component.compile(raise_errors: true)
|
9
|
+
@component.validate_collection_parameter!(validate_default: true)
|
8
10
|
|
9
11
|
@collection.map do |item|
|
10
|
-
@component.new(
|
12
|
+
content = @component.new(component_options(item, iterator)).render_in(view_context, &block)
|
13
|
+
iterator.iterate!
|
14
|
+
content
|
11
15
|
end.join.html_safe
|
12
16
|
end
|
13
17
|
|
@@ -26,5 +30,12 @@ module ViewComponent
|
|
26
30
|
raise ArgumentError.new("The value of the argument isn't a valid collection. Make sure it responds to to_ary: #{object.inspect}")
|
27
31
|
end
|
28
32
|
end
|
33
|
+
|
34
|
+
def component_options(item, iterator)
|
35
|
+
item_options = { @component.collection_parameter => item }
|
36
|
+
item_options[@component.collection_counter_parameter] = iterator.index + 1 if @component.counter_argument_present?
|
37
|
+
|
38
|
+
@options.merge(item_options)
|
39
|
+
end
|
29
40
|
end
|
30
41
|
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,8 +65,8 @@ 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
|
71
72
|
end
|
@@ -19,8 +19,12 @@ module ViewComponent # :nodoc:
|
|
19
19
|
end
|
20
20
|
|
21
21
|
# Returns the arguments for rendering of the component in its layout
|
22
|
-
def render_args(example)
|
23
|
-
|
22
|
+
def render_args(example, params: {})
|
23
|
+
example_params_names = instance_method(example).parameters.map(&:last)
|
24
|
+
provided_params = params.slice(*example_params_names).to_h.symbolize_keys
|
25
|
+
result = provided_params.empty? ? new.public_send(example) : new.public_send(example, **provided_params)
|
26
|
+
@layout = nil unless defined?(@layout)
|
27
|
+
result.merge(layout: @layout)
|
24
28
|
end
|
25
29
|
|
26
30
|
# Returns the component object class associated to the preview.
|
@@ -20,6 +20,16 @@ module ViewComponent # :nodoc:
|
|
20
20
|
# Defaults to +true+ for development environment
|
21
21
|
#
|
22
22
|
mattr_accessor :show_previews, instance_writer: false
|
23
|
+
|
24
|
+
# Set the entry route for component previews through app configuration:
|
25
|
+
#
|
26
|
+
# config.view_component.preview_route = "/previews"
|
27
|
+
#
|
28
|
+
# Defaults to +/rails/view_components+ when `show_previews' is enabled
|
29
|
+
#
|
30
|
+
mattr_accessor :preview_route, instance_writer: false do
|
31
|
+
"/rails/view_components"
|
32
|
+
end
|
23
33
|
end
|
24
34
|
end
|
25
35
|
end
|
@@ -7,7 +7,7 @@ module ViewComponent
|
|
7
7
|
include Capybara::Minitest::Assertions
|
8
8
|
|
9
9
|
def page
|
10
|
-
Capybara::Node::Simple.new(@
|
10
|
+
Capybara::Node::Simple.new(@rendered_component)
|
11
11
|
end
|
12
12
|
|
13
13
|
def refute_component_rendered
|
@@ -17,10 +17,12 @@ module ViewComponent
|
|
17
17
|
warn "WARNING in `ViewComponent::TestHelpers`: You must add `capybara` to your Gemfile to use Capybara assertions."
|
18
18
|
end
|
19
19
|
|
20
|
+
attr_reader :rendered_component
|
21
|
+
|
20
22
|
def render_inline(component, **args, &block)
|
21
|
-
@
|
23
|
+
@rendered_component = controller.view_context.render(component, args, &block)
|
22
24
|
|
23
|
-
Nokogiri::HTML.fragment(@
|
25
|
+
Nokogiri::HTML.fragment(@rendered_component)
|
24
26
|
end
|
25
27
|
|
26
28
|
def controller
|