actionview 5.2.4.4 → 6.1.1
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of actionview might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/CHANGELOG.md +221 -93
- data/MIT-LICENSE +1 -1
- data/README.rdoc +5 -3
- data/lib/action_view.rb +7 -2
- data/lib/action_view/base.rb +81 -15
- data/lib/action_view/buffers.rb +15 -0
- data/lib/action_view/cache_expiry.rb +52 -0
- data/lib/action_view/context.rb +5 -9
- data/lib/action_view/dependency_tracker.rb +10 -4
- data/lib/action_view/digestor.rb +15 -22
- data/lib/action_view/flows.rb +0 -1
- data/lib/action_view/gem_version.rb +4 -4
- data/lib/action_view/helpers.rb +0 -2
- data/lib/action_view/helpers/active_model_helper.rb +0 -1
- data/lib/action_view/helpers/asset_tag_helper.rb +63 -46
- data/lib/action_view/helpers/asset_url_helper.rb +9 -6
- data/lib/action_view/helpers/atom_feed_helper.rb +2 -1
- data/lib/action_view/helpers/cache_helper.rb +23 -22
- data/lib/action_view/helpers/capture_helper.rb +4 -0
- data/lib/action_view/helpers/csp_helper.rb +4 -2
- data/lib/action_view/helpers/csrf_helper.rb +1 -1
- data/lib/action_view/helpers/date_helper.rb +73 -30
- data/lib/action_view/helpers/form_helper.rb +305 -37
- data/lib/action_view/helpers/form_options_helper.rb +23 -23
- data/lib/action_view/helpers/form_tag_helper.rb +19 -16
- data/lib/action_view/helpers/javascript_helper.rb +12 -11
- data/lib/action_view/helpers/number_helper.rb +14 -8
- data/lib/action_view/helpers/output_safety_helper.rb +1 -1
- data/lib/action_view/helpers/rendering_helper.rb +17 -7
- data/lib/action_view/helpers/sanitize_helper.rb +12 -18
- data/lib/action_view/helpers/tag_helper.rb +98 -22
- data/lib/action_view/helpers/tags/base.rb +18 -11
- data/lib/action_view/helpers/tags/check_box.rb +0 -1
- data/lib/action_view/helpers/tags/collection_check_boxes.rb +0 -1
- data/lib/action_view/helpers/tags/collection_helpers.rb +0 -1
- data/lib/action_view/helpers/tags/collection_radio_buttons.rb +0 -1
- data/lib/action_view/helpers/tags/color_field.rb +1 -2
- data/lib/action_view/helpers/tags/date_field.rb +1 -2
- data/lib/action_view/helpers/tags/date_select.rb +2 -3
- data/lib/action_view/helpers/tags/datetime_field.rb +0 -1
- data/lib/action_view/helpers/tags/datetime_local_field.rb +1 -2
- data/lib/action_view/helpers/tags/label.rb +4 -1
- data/lib/action_view/helpers/tags/month_field.rb +1 -2
- data/lib/action_view/helpers/tags/radio_button.rb +0 -1
- data/lib/action_view/helpers/tags/select.rb +1 -2
- data/lib/action_view/helpers/tags/text_field.rb +0 -1
- data/lib/action_view/helpers/tags/time_field.rb +1 -2
- data/lib/action_view/helpers/tags/translator.rb +1 -6
- data/lib/action_view/helpers/tags/week_field.rb +1 -2
- data/lib/action_view/helpers/text_helper.rb +3 -4
- data/lib/action_view/helpers/translation_helper.rb +93 -55
- data/lib/action_view/helpers/url_helper.rb +121 -27
- data/lib/action_view/layouts.rb +8 -10
- data/lib/action_view/log_subscriber.rb +30 -15
- data/lib/action_view/lookup_context.rb +63 -35
- data/lib/action_view/path_set.rb +3 -12
- data/lib/action_view/railtie.rb +42 -26
- data/lib/action_view/record_identifier.rb +2 -3
- data/lib/action_view/renderer/abstract_renderer.rb +142 -11
- data/lib/action_view/renderer/collection_renderer.rb +196 -0
- data/lib/action_view/renderer/object_renderer.rb +34 -0
- data/lib/action_view/renderer/partial_renderer.rb +21 -273
- data/lib/action_view/renderer/partial_renderer/collection_caching.rb +61 -16
- data/lib/action_view/renderer/renderer.rb +59 -4
- data/lib/action_view/renderer/streaming_template_renderer.rb +10 -8
- data/lib/action_view/renderer/template_renderer.rb +35 -27
- data/lib/action_view/rendering.rb +54 -33
- data/lib/action_view/routing_url_for.rb +13 -12
- data/lib/action_view/template.rb +66 -75
- data/lib/action_view/template/error.rb +30 -15
- data/lib/action_view/template/handlers.rb +1 -1
- data/lib/action_view/template/handlers/builder.rb +2 -2
- data/lib/action_view/template/handlers/erb.rb +16 -11
- data/lib/action_view/template/handlers/erb/erubi.rb +15 -9
- data/lib/action_view/template/handlers/html.rb +1 -1
- data/lib/action_view/template/handlers/raw.rb +2 -2
- data/lib/action_view/template/html.rb +5 -6
- data/lib/action_view/template/inline.rb +22 -0
- data/lib/action_view/template/raw_file.rb +25 -0
- data/lib/action_view/template/renderable.rb +24 -0
- data/lib/action_view/template/resolver.rb +191 -150
- data/lib/action_view/template/sources.rb +13 -0
- data/lib/action_view/template/sources/file.rb +17 -0
- data/lib/action_view/template/text.rb +2 -3
- data/lib/action_view/test_case.rb +21 -29
- data/lib/action_view/testing/resolvers.rb +18 -27
- data/lib/action_view/unbound_template.rb +31 -0
- data/lib/action_view/view_paths.rb +59 -38
- data/lib/assets/compiled/rails-ujs.js +29 -3
- metadata +32 -21
- data/lib/action_view/helpers/record_tag_helper.rb +0 -23
@@ -59,8 +59,8 @@ module ActionView
|
|
59
59
|
|
60
60
|
include ModelNaming
|
61
61
|
|
62
|
-
JOIN = "_"
|
63
|
-
NEW = "new"
|
62
|
+
JOIN = "_"
|
63
|
+
NEW = "new"
|
64
64
|
|
65
65
|
# The DOM class convention is to use the singular form of an object or class.
|
66
66
|
#
|
@@ -95,7 +95,6 @@ module ActionView
|
|
95
95
|
end
|
96
96
|
|
97
97
|
private
|
98
|
-
|
99
98
|
# Returns a string representation of the key attribute(s) that is suitable for use in an HTML DOM id.
|
100
99
|
# This can be overwritten to customize the default generated string representation if desired.
|
101
100
|
# If you need to read back a key from a dom_id in order to query for the underlying database record,
|
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "concurrent/map"
|
4
|
+
|
3
5
|
module ActionView
|
4
6
|
# This class defines the interface for a renderer. Each class that
|
5
7
|
# subclasses +AbstractRenderer+ is used by the base +Renderer+ class to
|
@@ -14,10 +16,10 @@ module ActionView
|
|
14
16
|
#
|
15
17
|
# Whenever the +render+ method is called on the base +Renderer+ class, a new
|
16
18
|
# renderer object of the correct type is created, and the +render+ method on
|
17
|
-
# that new object is called in turn. This abstracts the
|
19
|
+
# that new object is called in turn. This abstracts the set up and rendering
|
18
20
|
# into a separate classes for partials and templates.
|
19
21
|
class AbstractRenderer #:nodoc:
|
20
|
-
delegate :
|
22
|
+
delegate :template_exists?, :any_templates?, :formats, to: :@lookup_context
|
21
23
|
|
22
24
|
def initialize(lookup_context)
|
23
25
|
@lookup_context = lookup_context
|
@@ -27,22 +29,143 @@ module ActionView
|
|
27
29
|
raise NotImplementedError
|
28
30
|
end
|
29
31
|
|
30
|
-
|
32
|
+
module ObjectRendering # :nodoc:
|
33
|
+
PREFIXED_PARTIAL_NAMES = Concurrent::Map.new do |h, k|
|
34
|
+
h[k] = Concurrent::Map.new
|
35
|
+
end
|
31
36
|
|
32
|
-
def
|
33
|
-
|
34
|
-
|
37
|
+
def initialize(lookup_context, options)
|
38
|
+
super
|
39
|
+
@context_prefix = lookup_context.prefixes.first
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
def local_variable(path)
|
44
|
+
if as = @options[:as]
|
45
|
+
raise_invalid_option_as(as) unless /\A[a-z_]\w*\z/.match?(as.to_s)
|
46
|
+
as.to_sym
|
47
|
+
else
|
48
|
+
base = path.end_with?("/") ? "" : File.basename(path)
|
49
|
+
raise_invalid_identifier(path) unless base =~ /\A_?(.*?)(?:\.\w+)*\z/
|
50
|
+
$1.to_sym
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
IDENTIFIER_ERROR_MESSAGE = "The partial name (%s) is not a valid Ruby identifier; " \
|
55
|
+
"make sure your partial name starts with underscore."
|
56
|
+
|
57
|
+
OPTION_AS_ERROR_MESSAGE = "The value (%s) of the option `as` is not a valid Ruby identifier; " \
|
58
|
+
"make sure it starts with lowercase letter, " \
|
59
|
+
"and is followed by any combination of letters, numbers and underscores."
|
60
|
+
|
61
|
+
def raise_invalid_identifier(path)
|
62
|
+
raise ArgumentError, IDENTIFIER_ERROR_MESSAGE % path
|
63
|
+
end
|
64
|
+
|
65
|
+
def raise_invalid_option_as(as)
|
66
|
+
raise ArgumentError, OPTION_AS_ERROR_MESSAGE % as
|
67
|
+
end
|
68
|
+
|
69
|
+
# Obtains the path to where the object's partial is located. If the object
|
70
|
+
# responds to +to_partial_path+, then +to_partial_path+ will be called and
|
71
|
+
# will provide the path. If the object does not respond to +to_partial_path+,
|
72
|
+
# then an +ArgumentError+ is raised.
|
73
|
+
#
|
74
|
+
# If +prefix_partial_path_with_controller_namespace+ is true, then this
|
75
|
+
# method will prefix the partial paths with a namespace.
|
76
|
+
def partial_path(object, view)
|
77
|
+
object = object.to_model if object.respond_to?(:to_model)
|
78
|
+
|
79
|
+
path = if object.respond_to?(:to_partial_path)
|
80
|
+
object.to_partial_path
|
81
|
+
else
|
82
|
+
raise ArgumentError.new("'#{object.inspect}' is not an ActiveModel-compatible object. It must implement :to_partial_path.")
|
83
|
+
end
|
84
|
+
|
85
|
+
if view.prefix_partial_path_with_controller_namespace
|
86
|
+
PREFIXED_PARTIAL_NAMES[@context_prefix][path] ||= merge_prefix_into_object_path(@context_prefix, path.dup)
|
87
|
+
else
|
88
|
+
path
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def merge_prefix_into_object_path(prefix, object_path)
|
93
|
+
if prefix.include?(?/) && object_path.include?(?/)
|
94
|
+
prefixes = []
|
95
|
+
prefix_array = File.dirname(prefix).split("/")
|
96
|
+
object_path_array = object_path.split("/")[0..-3] # skip model dir & partial
|
97
|
+
|
98
|
+
prefix_array.each_with_index do |dir, index|
|
99
|
+
break if dir == object_path_array[index]
|
100
|
+
prefixes << dir
|
101
|
+
end
|
102
|
+
|
103
|
+
(prefixes << object_path).join("/")
|
104
|
+
else
|
105
|
+
object_path
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
class RenderedCollection # :nodoc:
|
111
|
+
def self.empty(format)
|
112
|
+
EmptyCollection.new format
|
113
|
+
end
|
35
114
|
|
36
|
-
|
115
|
+
attr_reader :rendered_templates
|
116
|
+
|
117
|
+
def initialize(rendered_templates, spacer)
|
118
|
+
@rendered_templates = rendered_templates
|
119
|
+
@spacer = spacer
|
120
|
+
end
|
121
|
+
|
122
|
+
def body
|
123
|
+
@rendered_templates.map(&:body).join(@spacer.body).html_safe
|
124
|
+
end
|
125
|
+
|
126
|
+
def format
|
127
|
+
rendered_templates.first.format
|
128
|
+
end
|
129
|
+
|
130
|
+
class EmptyCollection
|
131
|
+
attr_reader :format
|
132
|
+
|
133
|
+
def initialize(format)
|
134
|
+
@format = format
|
37
135
|
end
|
136
|
+
|
137
|
+
def body; nil; end
|
38
138
|
end
|
139
|
+
end
|
39
140
|
|
40
|
-
|
41
|
-
|
141
|
+
class RenderedTemplate # :nodoc:
|
142
|
+
attr_reader :body, :template
|
42
143
|
|
43
|
-
|
44
|
-
|
144
|
+
def initialize(body, template)
|
145
|
+
@body = body
|
146
|
+
@template = template
|
147
|
+
end
|
148
|
+
|
149
|
+
def format
|
150
|
+
template.format
|
151
|
+
end
|
152
|
+
|
153
|
+
EMPTY_SPACER = Struct.new(:body).new
|
154
|
+
end
|
155
|
+
|
156
|
+
private
|
157
|
+
NO_DETAILS = {}.freeze
|
158
|
+
|
159
|
+
def extract_details(options) # :doc:
|
160
|
+
details = nil
|
161
|
+
@lookup_context.registered_details.each do |key|
|
162
|
+
value = options[key]
|
163
|
+
|
164
|
+
if value
|
165
|
+
(details ||= {})[key] = Array(value)
|
166
|
+
end
|
45
167
|
end
|
168
|
+
details || NO_DETAILS
|
46
169
|
end
|
47
170
|
|
48
171
|
def prepend_formats(formats) # :doc:
|
@@ -51,5 +174,13 @@ module ActionView
|
|
51
174
|
|
52
175
|
@lookup_context.formats = formats | @lookup_context.formats
|
53
176
|
end
|
177
|
+
|
178
|
+
def build_rendered_template(content, template)
|
179
|
+
RenderedTemplate.new content, template
|
180
|
+
end
|
181
|
+
|
182
|
+
def build_rendered_collection(templates, spacer)
|
183
|
+
RenderedCollection.new templates, spacer
|
184
|
+
end
|
54
185
|
end
|
55
186
|
end
|
@@ -0,0 +1,196 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "action_view/renderer/partial_renderer"
|
4
|
+
|
5
|
+
module ActionView
|
6
|
+
class PartialIteration
|
7
|
+
# The number of iterations that will be done by the partial.
|
8
|
+
attr_reader :size
|
9
|
+
|
10
|
+
# The current iteration of the partial.
|
11
|
+
attr_reader :index
|
12
|
+
|
13
|
+
def initialize(size)
|
14
|
+
@size = size
|
15
|
+
@index = 0
|
16
|
+
end
|
17
|
+
|
18
|
+
# Check if this is the first iteration of the partial.
|
19
|
+
def first?
|
20
|
+
index == 0
|
21
|
+
end
|
22
|
+
|
23
|
+
# Check if this is the last iteration of the partial.
|
24
|
+
def last?
|
25
|
+
index == size - 1
|
26
|
+
end
|
27
|
+
|
28
|
+
def iterate! # :nodoc:
|
29
|
+
@index += 1
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
class CollectionRenderer < PartialRenderer # :nodoc:
|
34
|
+
include ObjectRendering
|
35
|
+
|
36
|
+
class CollectionIterator # :nodoc:
|
37
|
+
include Enumerable
|
38
|
+
|
39
|
+
def initialize(collection)
|
40
|
+
@collection = collection
|
41
|
+
end
|
42
|
+
|
43
|
+
def each(&blk)
|
44
|
+
@collection.each(&blk)
|
45
|
+
end
|
46
|
+
|
47
|
+
def size
|
48
|
+
@collection.size
|
49
|
+
end
|
50
|
+
|
51
|
+
def length
|
52
|
+
@collection.respond_to?(:length) ? @collection.length : size
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
class SameCollectionIterator < CollectionIterator # :nodoc:
|
57
|
+
def initialize(collection, path, variables)
|
58
|
+
super(collection)
|
59
|
+
@path = path
|
60
|
+
@variables = variables
|
61
|
+
end
|
62
|
+
|
63
|
+
def from_collection(collection)
|
64
|
+
self.class.new(collection, @path, @variables)
|
65
|
+
end
|
66
|
+
|
67
|
+
def each_with_info
|
68
|
+
return enum_for(:each_with_info) unless block_given?
|
69
|
+
variables = [@path] + @variables
|
70
|
+
@collection.each { |o| yield(o, variables) }
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
class PreloadCollectionIterator < SameCollectionIterator # :nodoc:
|
75
|
+
def initialize(collection, path, variables, relation)
|
76
|
+
super(collection, path, variables)
|
77
|
+
relation.skip_preloading! unless relation.loaded?
|
78
|
+
@relation = relation
|
79
|
+
end
|
80
|
+
|
81
|
+
def from_collection(collection)
|
82
|
+
self.class.new(collection, @path, @variables, @relation)
|
83
|
+
end
|
84
|
+
|
85
|
+
def each_with_info
|
86
|
+
return super unless block_given?
|
87
|
+
@relation.preload_associations(@collection)
|
88
|
+
super
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
class MixedCollectionIterator < CollectionIterator # :nodoc:
|
93
|
+
def initialize(collection, paths)
|
94
|
+
super(collection)
|
95
|
+
@paths = paths
|
96
|
+
end
|
97
|
+
|
98
|
+
def each_with_info
|
99
|
+
return enum_for(:each_with_info) unless block_given?
|
100
|
+
@collection.each_with_index { |o, i| yield(o, @paths[i]) }
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def render_collection_with_partial(collection, partial, context, block)
|
105
|
+
iter_vars = retrieve_variable(partial)
|
106
|
+
|
107
|
+
collection = if collection.respond_to?(:preload_associations)
|
108
|
+
PreloadCollectionIterator.new(collection, partial, iter_vars, collection)
|
109
|
+
else
|
110
|
+
SameCollectionIterator.new(collection, partial, iter_vars)
|
111
|
+
end
|
112
|
+
|
113
|
+
template = find_template(partial, @locals.keys + iter_vars)
|
114
|
+
|
115
|
+
layout = if !block && (layout = @options[:layout])
|
116
|
+
find_template(layout.to_s, @locals.keys + iter_vars)
|
117
|
+
end
|
118
|
+
|
119
|
+
render_collection(collection, context, partial, template, layout, block)
|
120
|
+
end
|
121
|
+
|
122
|
+
def render_collection_derive_partial(collection, context, block)
|
123
|
+
paths = collection.map { |o| partial_path(o, context) }
|
124
|
+
|
125
|
+
if paths.uniq.length == 1
|
126
|
+
# Homogeneous
|
127
|
+
render_collection_with_partial(collection, paths.first, context, block)
|
128
|
+
else
|
129
|
+
if @options[:cached]
|
130
|
+
raise NotImplementedError, "render caching requires a template. Please specify a partial when rendering"
|
131
|
+
end
|
132
|
+
|
133
|
+
paths.map! { |path| retrieve_variable(path).unshift(path) }
|
134
|
+
collection = MixedCollectionIterator.new(collection, paths)
|
135
|
+
render_collection(collection, context, nil, nil, nil, block)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
private
|
140
|
+
def retrieve_variable(path)
|
141
|
+
variable = local_variable(path)
|
142
|
+
[variable, :"#{variable}_counter", :"#{variable}_iteration"]
|
143
|
+
end
|
144
|
+
|
145
|
+
def render_collection(collection, view, path, template, layout, block)
|
146
|
+
identifier = (template && template.identifier) || path
|
147
|
+
ActiveSupport::Notifications.instrument(
|
148
|
+
"render_collection.action_view",
|
149
|
+
identifier: identifier,
|
150
|
+
layout: layout && layout.virtual_path,
|
151
|
+
count: collection.length
|
152
|
+
) do |payload|
|
153
|
+
spacer = if @options.key?(:spacer_template)
|
154
|
+
spacer_template = find_template(@options[:spacer_template], @locals.keys)
|
155
|
+
build_rendered_template(spacer_template.render(view, @locals), spacer_template)
|
156
|
+
else
|
157
|
+
RenderedTemplate::EMPTY_SPACER
|
158
|
+
end
|
159
|
+
|
160
|
+
collection_body = if template
|
161
|
+
cache_collection_render(payload, view, template, collection) do |filtered_collection|
|
162
|
+
collection_with_template(view, template, layout, filtered_collection)
|
163
|
+
end
|
164
|
+
else
|
165
|
+
collection_with_template(view, nil, layout, collection)
|
166
|
+
end
|
167
|
+
|
168
|
+
return RenderedCollection.empty(@lookup_context.formats.first) if collection_body.empty?
|
169
|
+
|
170
|
+
build_rendered_collection(collection_body, spacer)
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
def collection_with_template(view, template, layout, collection)
|
175
|
+
locals = @locals
|
176
|
+
cache = {}
|
177
|
+
|
178
|
+
partial_iteration = PartialIteration.new(collection.size)
|
179
|
+
|
180
|
+
collection.each_with_info.map do |object, (path, as, counter, iteration)|
|
181
|
+
index = partial_iteration.index
|
182
|
+
|
183
|
+
locals[as] = object
|
184
|
+
locals[counter] = index
|
185
|
+
locals[iteration] = partial_iteration
|
186
|
+
|
187
|
+
_template = (cache[path] ||= (template || find_template(path, @locals.keys + [as, counter, iteration])))
|
188
|
+
|
189
|
+
content = _template.render(view, locals)
|
190
|
+
content = layout.render(view, locals) { content } if layout
|
191
|
+
partial_iteration.iterate!
|
192
|
+
build_rendered_template(content, _template)
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionView
|
4
|
+
class ObjectRenderer < PartialRenderer # :nodoc:
|
5
|
+
include ObjectRendering
|
6
|
+
|
7
|
+
def initialize(lookup_context, options)
|
8
|
+
super
|
9
|
+
@object = nil
|
10
|
+
@local_name = nil
|
11
|
+
end
|
12
|
+
|
13
|
+
def render_object_with_partial(object, partial, context, block)
|
14
|
+
@object = object
|
15
|
+
@local_name = local_variable(partial)
|
16
|
+
render(partial, context, block)
|
17
|
+
end
|
18
|
+
|
19
|
+
def render_object_derive_partial(object, context, block)
|
20
|
+
path = partial_path(object, context)
|
21
|
+
render_object_with_partial(object, path, context, block)
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
def template_keys(path)
|
26
|
+
super + [@local_name]
|
27
|
+
end
|
28
|
+
|
29
|
+
def render_partial_template(view, locals, template, layout, block)
|
30
|
+
locals[@local_name || template.variable] = @object
|
31
|
+
super(view, locals, template, layout, block)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -1,36 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "concurrent/map"
|
4
3
|
require "action_view/renderer/partial_renderer/collection_caching"
|
5
4
|
|
6
5
|
module ActionView
|
7
|
-
class PartialIteration
|
8
|
-
# The number of iterations that will be done by the partial.
|
9
|
-
attr_reader :size
|
10
|
-
|
11
|
-
# The current iteration of the partial.
|
12
|
-
attr_reader :index
|
13
|
-
|
14
|
-
def initialize(size)
|
15
|
-
@size = size
|
16
|
-
@index = 0
|
17
|
-
end
|
18
|
-
|
19
|
-
# Check if this is the first iteration of the partial.
|
20
|
-
def first?
|
21
|
-
index == 0
|
22
|
-
end
|
23
|
-
|
24
|
-
# Check if this is the last iteration of the partial.
|
25
|
-
def last?
|
26
|
-
index == size - 1
|
27
|
-
end
|
28
|
-
|
29
|
-
def iterate! # :nodoc:
|
30
|
-
@index += 1
|
31
|
-
end
|
32
|
-
end
|
33
|
-
|
34
6
|
# = Action View Partials
|
35
7
|
#
|
36
8
|
# There's also a convenience method for rendering sub templates within the current controller that depends on a
|
@@ -105,9 +77,6 @@ module ActionView
|
|
105
77
|
#
|
106
78
|
# <%= render(partial: "ad", collection: @advertisements) || "There's no ad to be displayed" %>
|
107
79
|
#
|
108
|
-
# NOTE: Due to backwards compatibility concerns, the collection can't be one of hashes. Normally you'd also
|
109
|
-
# just keep domain objects, like Active Records, in there.
|
110
|
-
#
|
111
80
|
# == \Rendering shared partials
|
112
81
|
#
|
113
82
|
# Two controllers can share a set of partials and render them like this:
|
@@ -285,268 +254,47 @@ module ActionView
|
|
285
254
|
class PartialRenderer < AbstractRenderer
|
286
255
|
include CollectionCaching
|
287
256
|
|
288
|
-
|
289
|
-
|
257
|
+
def initialize(lookup_context, options)
|
258
|
+
super(lookup_context)
|
259
|
+
@options = options
|
260
|
+
@locals = @options[:locals] || {}
|
261
|
+
@details = extract_details(@options)
|
290
262
|
end
|
291
263
|
|
292
|
-
def
|
293
|
-
|
294
|
-
@context_prefix = @lookup_context.prefixes.first
|
295
|
-
end
|
296
|
-
|
297
|
-
def render(context, options, block)
|
298
|
-
setup(context, options, block)
|
299
|
-
@template = find_partial
|
264
|
+
def render(partial, context, block)
|
265
|
+
template = find_template(partial, template_keys(partial))
|
300
266
|
|
301
|
-
|
302
|
-
|
303
|
-
@template.formats.first
|
304
|
-
else
|
305
|
-
formats.first
|
306
|
-
end
|
267
|
+
if !block && (layout = @options[:layout])
|
268
|
+
layout = find_template(layout.to_s, template_keys(partial))
|
307
269
|
end
|
308
270
|
|
309
|
-
|
310
|
-
render_collection
|
311
|
-
else
|
312
|
-
render_partial
|
313
|
-
end
|
271
|
+
render_partial_template(context, @locals, template, layout, block)
|
314
272
|
end
|
315
273
|
|
316
274
|
private
|
317
|
-
|
318
|
-
|
319
|
-
instrument(:collection, count: @collection.size) do |payload|
|
320
|
-
return nil if @collection.blank?
|
321
|
-
|
322
|
-
if @options.key?(:spacer_template)
|
323
|
-
spacer = find_template(@options[:spacer_template], @locals.keys).render(@view, @locals)
|
324
|
-
end
|
325
|
-
|
326
|
-
cache_collection_render(payload) do
|
327
|
-
@template ? collection_with_template : collection_without_template
|
328
|
-
end.join(spacer).html_safe
|
329
|
-
end
|
275
|
+
def template_keys(_)
|
276
|
+
@locals.keys
|
330
277
|
end
|
331
278
|
|
332
|
-
def
|
333
|
-
instrument(
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
end
|
340
|
-
|
341
|
-
object = locals[as] if object.nil? # Respect object when object is false
|
342
|
-
locals[as] = object if @has_object
|
343
|
-
|
344
|
-
content = @template.render(view, locals) do |*name|
|
279
|
+
def render_partial_template(view, locals, template, layout, block)
|
280
|
+
ActiveSupport::Notifications.instrument(
|
281
|
+
"render_partial.action_view",
|
282
|
+
identifier: template.identifier,
|
283
|
+
layout: layout && layout.virtual_path
|
284
|
+
) do |payload|
|
285
|
+
content = template.render(view, locals, add_to_stack: !block) do |*name|
|
345
286
|
view._layout_for(*name, &block)
|
346
287
|
end
|
347
288
|
|
348
289
|
content = layout.render(view, locals) { content } if layout
|
349
|
-
payload[:cache_hit] = view.view_renderer.cache_hits[
|
350
|
-
content
|
351
|
-
end
|
352
|
-
end
|
353
|
-
|
354
|
-
# Sets up instance variables needed for rendering a partial. This method
|
355
|
-
# finds the options and details and extracts them. The method also contains
|
356
|
-
# logic that handles the type of object passed in as the partial.
|
357
|
-
#
|
358
|
-
# If +options[:partial]+ is a string, then the <tt>@path</tt> instance variable is
|
359
|
-
# set to that string. Otherwise, the +options[:partial]+ object must
|
360
|
-
# respond to +to_partial_path+ in order to setup the path.
|
361
|
-
def setup(context, options, block)
|
362
|
-
@view = context
|
363
|
-
@options = options
|
364
|
-
@block = block
|
365
|
-
|
366
|
-
@locals = options[:locals] || {}
|
367
|
-
@details = extract_details(options)
|
368
|
-
|
369
|
-
prepend_formats(options[:formats])
|
370
|
-
|
371
|
-
partial = options[:partial]
|
372
|
-
|
373
|
-
if String === partial
|
374
|
-
@has_object = options.key?(:object)
|
375
|
-
@object = options[:object]
|
376
|
-
@collection = collection_from_options
|
377
|
-
@path = partial
|
378
|
-
else
|
379
|
-
@has_object = true
|
380
|
-
@object = partial
|
381
|
-
@collection = collection_from_object || collection_from_options
|
382
|
-
|
383
|
-
if @collection
|
384
|
-
paths = @collection_data = @collection.map { |o| partial_path(o) }
|
385
|
-
@path = paths.uniq.one? ? paths.first : nil
|
386
|
-
else
|
387
|
-
@path = partial_path
|
388
|
-
end
|
290
|
+
payload[:cache_hit] = view.view_renderer.cache_hits[template.virtual_path]
|
291
|
+
build_rendered_template(content, template)
|
389
292
|
end
|
390
|
-
|
391
|
-
if as = options[:as]
|
392
|
-
raise_invalid_option_as(as) unless /\A[a-z_]\w*\z/.match?(as.to_s)
|
393
|
-
as = as.to_sym
|
394
|
-
end
|
395
|
-
|
396
|
-
if @path
|
397
|
-
@variable, @variable_counter, @variable_iteration = retrieve_variable(@path, as)
|
398
|
-
@template_keys = retrieve_template_keys
|
399
|
-
else
|
400
|
-
paths.map! { |path| retrieve_variable(path, as).unshift(path) }
|
401
|
-
end
|
402
|
-
|
403
|
-
self
|
404
|
-
end
|
405
|
-
|
406
|
-
def collection_from_options
|
407
|
-
if @options.key?(:collection)
|
408
|
-
collection = @options[:collection]
|
409
|
-
collection ? collection.to_a : []
|
410
|
-
end
|
411
|
-
end
|
412
|
-
|
413
|
-
def collection_from_object
|
414
|
-
@object.to_ary if @object.respond_to?(:to_ary)
|
415
|
-
end
|
416
|
-
|
417
|
-
def find_partial
|
418
|
-
find_template(@path, @template_keys) if @path
|
419
293
|
end
|
420
294
|
|
421
295
|
def find_template(path, locals)
|
422
296
|
prefixes = path.include?(?/) ? [] : @lookup_context.prefixes
|
423
297
|
@lookup_context.find_template(path, prefixes, true, locals, @details)
|
424
298
|
end
|
425
|
-
|
426
|
-
def collection_with_template
|
427
|
-
view, locals, template = @view, @locals, @template
|
428
|
-
as, counter, iteration = @variable, @variable_counter, @variable_iteration
|
429
|
-
|
430
|
-
if layout = @options[:layout]
|
431
|
-
layout = find_template(layout, @template_keys)
|
432
|
-
end
|
433
|
-
|
434
|
-
partial_iteration = PartialIteration.new(@collection.size)
|
435
|
-
locals[iteration] = partial_iteration
|
436
|
-
|
437
|
-
@collection.map do |object|
|
438
|
-
locals[as] = object
|
439
|
-
locals[counter] = partial_iteration.index
|
440
|
-
|
441
|
-
content = template.render(view, locals)
|
442
|
-
content = layout.render(view, locals) { content } if layout
|
443
|
-
partial_iteration.iterate!
|
444
|
-
content
|
445
|
-
end
|
446
|
-
end
|
447
|
-
|
448
|
-
def collection_without_template
|
449
|
-
view, locals, collection_data = @view, @locals, @collection_data
|
450
|
-
cache = {}
|
451
|
-
keys = @locals.keys
|
452
|
-
|
453
|
-
partial_iteration = PartialIteration.new(@collection.size)
|
454
|
-
|
455
|
-
@collection.map do |object|
|
456
|
-
index = partial_iteration.index
|
457
|
-
path, as, counter, iteration = collection_data[index]
|
458
|
-
|
459
|
-
locals[as] = object
|
460
|
-
locals[counter] = index
|
461
|
-
locals[iteration] = partial_iteration
|
462
|
-
|
463
|
-
template = (cache[path] ||= find_template(path, keys + [as, counter, iteration]))
|
464
|
-
content = template.render(view, locals)
|
465
|
-
partial_iteration.iterate!
|
466
|
-
content
|
467
|
-
end
|
468
|
-
end
|
469
|
-
|
470
|
-
# Obtains the path to where the object's partial is located. If the object
|
471
|
-
# responds to +to_partial_path+, then +to_partial_path+ will be called and
|
472
|
-
# will provide the path. If the object does not respond to +to_partial_path+,
|
473
|
-
# then an +ArgumentError+ is raised.
|
474
|
-
#
|
475
|
-
# If +prefix_partial_path_with_controller_namespace+ is true, then this
|
476
|
-
# method will prefix the partial paths with a namespace.
|
477
|
-
def partial_path(object = @object)
|
478
|
-
object = object.to_model if object.respond_to?(:to_model)
|
479
|
-
|
480
|
-
path = if object.respond_to?(:to_partial_path)
|
481
|
-
object.to_partial_path
|
482
|
-
else
|
483
|
-
raise ArgumentError.new("'#{object.inspect}' is not an ActiveModel-compatible object. It must implement :to_partial_path.")
|
484
|
-
end
|
485
|
-
|
486
|
-
if @view.prefix_partial_path_with_controller_namespace
|
487
|
-
prefixed_partial_names[path] ||= merge_prefix_into_object_path(@context_prefix, path.dup)
|
488
|
-
else
|
489
|
-
path
|
490
|
-
end
|
491
|
-
end
|
492
|
-
|
493
|
-
def prefixed_partial_names
|
494
|
-
@prefixed_partial_names ||= PREFIXED_PARTIAL_NAMES[@context_prefix]
|
495
|
-
end
|
496
|
-
|
497
|
-
def merge_prefix_into_object_path(prefix, object_path)
|
498
|
-
if prefix.include?(?/) && object_path.include?(?/)
|
499
|
-
prefixes = []
|
500
|
-
prefix_array = File.dirname(prefix).split("/")
|
501
|
-
object_path_array = object_path.split("/")[0..-3] # skip model dir & partial
|
502
|
-
|
503
|
-
prefix_array.each_with_index do |dir, index|
|
504
|
-
break if dir == object_path_array[index]
|
505
|
-
prefixes << dir
|
506
|
-
end
|
507
|
-
|
508
|
-
(prefixes << object_path).join("/")
|
509
|
-
else
|
510
|
-
object_path
|
511
|
-
end
|
512
|
-
end
|
513
|
-
|
514
|
-
def retrieve_template_keys
|
515
|
-
keys = @locals.keys
|
516
|
-
keys << @variable if @has_object || @collection
|
517
|
-
if @collection
|
518
|
-
keys << @variable_counter
|
519
|
-
keys << @variable_iteration
|
520
|
-
end
|
521
|
-
keys
|
522
|
-
end
|
523
|
-
|
524
|
-
def retrieve_variable(path, as)
|
525
|
-
variable = as || begin
|
526
|
-
base = path[-1] == "/".freeze ? "".freeze : File.basename(path)
|
527
|
-
raise_invalid_identifier(path) unless base =~ /\A_?(.*?)(?:\.\w+)*\z/
|
528
|
-
$1.to_sym
|
529
|
-
end
|
530
|
-
if @collection
|
531
|
-
variable_counter = :"#{variable}_counter"
|
532
|
-
variable_iteration = :"#{variable}_iteration"
|
533
|
-
end
|
534
|
-
[variable, variable_counter, variable_iteration]
|
535
|
-
end
|
536
|
-
|
537
|
-
IDENTIFIER_ERROR_MESSAGE = "The partial name (%s) is not a valid Ruby identifier; " \
|
538
|
-
"make sure your partial name starts with underscore."
|
539
|
-
|
540
|
-
OPTION_AS_ERROR_MESSAGE = "The value (%s) of the option `as` is not a valid Ruby identifier; " \
|
541
|
-
"make sure it starts with lowercase letter, " \
|
542
|
-
"and is followed by any combination of letters, numbers and underscores."
|
543
|
-
|
544
|
-
def raise_invalid_identifier(path)
|
545
|
-
raise ArgumentError.new(IDENTIFIER_ERROR_MESSAGE % (path))
|
546
|
-
end
|
547
|
-
|
548
|
-
def raise_invalid_option_as(as)
|
549
|
-
raise ArgumentError.new(OPTION_AS_ERROR_MESSAGE % (as))
|
550
|
-
end
|
551
299
|
end
|
552
300
|
end
|