actionview 4.1.13 → 6.1.3.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 +5 -5
- data/CHANGELOG.md +181 -359
- data/MIT-LICENSE +1 -1
- data/README.rdoc +12 -6
- data/lib/action_view/base.rb +115 -43
- data/lib/action_view/buffers.rb +22 -4
- data/lib/action_view/cache_expiry.rb +52 -0
- data/lib/action_view/context.rb +8 -12
- data/lib/action_view/dependency_tracker.rb +61 -21
- data/lib/action_view/digestor.rb +89 -84
- data/lib/action_view/flows.rb +12 -13
- data/lib/action_view/gem_version.rb +6 -4
- data/lib/action_view/helpers/active_model_helper.rb +16 -11
- data/lib/action_view/helpers/asset_tag_helper.rb +311 -105
- data/lib/action_view/helpers/asset_url_helper.rb +197 -80
- data/lib/action_view/helpers/atom_feed_helper.rb +20 -17
- data/lib/action_view/helpers/cache_helper.rb +109 -45
- data/lib/action_view/helpers/capture_helper.rb +20 -22
- data/lib/action_view/helpers/controller_helper.rb +15 -4
- data/lib/action_view/helpers/csp_helper.rb +26 -0
- data/lib/action_view/helpers/csrf_helper.rb +8 -6
- data/lib/action_view/helpers/date_helper.rb +245 -140
- data/lib/action_view/helpers/debug_helper.rb +14 -17
- data/lib/action_view/helpers/form_helper.rb +875 -148
- data/lib/action_view/helpers/form_options_helper.rb +128 -82
- data/lib/action_view/helpers/form_tag_helper.rb +253 -91
- data/lib/action_view/helpers/javascript_helper.rb +37 -15
- data/lib/action_view/helpers/number_helper.rb +100 -77
- data/lib/action_view/helpers/output_safety_helper.rb +42 -10
- data/lib/action_view/helpers/rendering_helper.rb +26 -15
- data/lib/action_view/helpers/sanitize_helper.rb +79 -164
- data/lib/action_view/helpers/tag_helper.rb +277 -64
- data/lib/action_view/helpers/tags/base.rb +143 -92
- data/lib/action_view/helpers/tags/check_box.rb +20 -19
- data/lib/action_view/helpers/tags/checkable.rb +4 -2
- data/lib/action_view/helpers/tags/collection_check_boxes.rb +12 -30
- data/lib/action_view/helpers/tags/collection_helpers.rb +69 -36
- data/lib/action_view/helpers/tags/collection_radio_buttons.rb +6 -12
- data/lib/action_view/helpers/tags/collection_select.rb +4 -2
- data/lib/action_view/helpers/tags/color_field.rb +4 -3
- data/lib/action_view/helpers/tags/date_field.rb +3 -2
- data/lib/action_view/helpers/tags/date_select.rb +38 -37
- data/lib/action_view/helpers/tags/datetime_field.rb +14 -5
- data/lib/action_view/helpers/tags/datetime_local_field.rb +3 -2
- data/lib/action_view/helpers/tags/datetime_select.rb +2 -0
- data/lib/action_view/helpers/tags/email_field.rb +2 -0
- data/lib/action_view/helpers/tags/file_field.rb +2 -0
- data/lib/action_view/helpers/tags/grouped_collection_select.rb +4 -2
- data/lib/action_view/helpers/tags/hidden_field.rb +2 -0
- data/lib/action_view/helpers/tags/label.rb +41 -22
- data/lib/action_view/helpers/tags/month_field.rb +3 -2
- data/lib/action_view/helpers/tags/number_field.rb +2 -0
- data/lib/action_view/helpers/tags/password_field.rb +3 -1
- data/lib/action_view/helpers/tags/placeholderable.rb +24 -0
- data/lib/action_view/helpers/tags/radio_button.rb +7 -6
- data/lib/action_view/helpers/tags/range_field.rb +2 -0
- data/lib/action_view/helpers/tags/search_field.rb +3 -0
- data/lib/action_view/helpers/tags/select.rb +11 -10
- data/lib/action_view/helpers/tags/tel_field.rb +2 -0
- data/lib/action_view/helpers/tags/text_area.rb +7 -1
- data/lib/action_view/helpers/tags/text_field.rb +11 -7
- data/lib/action_view/helpers/tags/time_field.rb +3 -2
- data/lib/action_view/helpers/tags/time_select.rb +2 -0
- data/lib/action_view/helpers/tags/time_zone_select.rb +3 -1
- data/lib/action_view/helpers/tags/translator.rb +39 -0
- data/lib/action_view/helpers/tags/url_field.rb +2 -0
- data/lib/action_view/helpers/tags/week_field.rb +3 -2
- data/lib/action_view/helpers/tags.rb +4 -1
- data/lib/action_view/helpers/text_helper.rb +80 -45
- data/lib/action_view/helpers/translation_helper.rb +148 -67
- data/lib/action_view/helpers/url_helper.rb +289 -147
- data/lib/action_view/helpers.rb +5 -3
- data/lib/action_view/layouts.rb +68 -63
- data/lib/action_view/log_subscriber.rb +80 -13
- data/lib/action_view/lookup_context.rb +137 -92
- data/lib/action_view/model_naming.rb +4 -2
- data/lib/action_view/path_set.rb +30 -16
- data/lib/action_view/railtie.rb +62 -13
- data/lib/action_view/record_identifier.rb +53 -26
- data/lib/action_view/renderer/abstract_renderer.rb +152 -13
- 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/collection_caching.rb +102 -0
- data/lib/action_view/renderer/partial_renderer.rb +61 -261
- data/lib/action_view/renderer/renderer.rb +67 -6
- data/lib/action_view/renderer/streaming_template_renderer.rb +58 -54
- data/lib/action_view/renderer/template_renderer.rb +83 -75
- data/lib/action_view/rendering.rb +73 -46
- data/lib/action_view/routing_url_for.rb +54 -17
- data/lib/action_view/tasks/cache_digests.rake +25 -0
- data/lib/action_view/template/error.rb +44 -29
- data/lib/action_view/template/handlers/builder.rb +12 -13
- data/lib/action_view/template/handlers/erb/erubi.rb +89 -0
- data/lib/action_view/template/handlers/erb.rb +23 -89
- data/lib/action_view/template/handlers/html.rb +11 -0
- data/lib/action_view/template/handlers/raw.rb +4 -4
- data/lib/action_view/template/handlers.rb +22 -9
- data/lib/action_view/template/html.rb +10 -11
- 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 +267 -181
- data/lib/action_view/template/sources/file.rb +17 -0
- data/lib/action_view/template/sources.rb +13 -0
- data/lib/action_view/template/text.rb +8 -10
- data/lib/action_view/template/types.rb +18 -18
- data/lib/action_view/template.rb +109 -99
- data/lib/action_view/test_case.rb +73 -53
- data/lib/action_view/testing/resolvers.rb +24 -33
- data/lib/action_view/unbound_template.rb +31 -0
- data/lib/action_view/version.rb +3 -1
- data/lib/action_view/view_paths.rb +74 -44
- data/lib/action_view.rb +14 -9
- data/lib/assets/compiled/rails-ujs.js +746 -0
- metadata +71 -26
- data/lib/action_view/helpers/record_tag_helper.rb +0 -108
- data/lib/action_view/tasks/dependencies.rake +0 -23
- data/lib/action_view/vendor/html-scanner/html/document.rb +0 -68
- data/lib/action_view/vendor/html-scanner/html/node.rb +0 -532
- data/lib/action_view/vendor/html-scanner/html/sanitizer.rb +0 -188
- data/lib/action_view/vendor/html-scanner/html/selector.rb +0 -830
- data/lib/action_view/vendor/html-scanner/html/tokenizer.rb +0 -107
- data/lib/action_view/vendor/html-scanner/html/version.rb +0 -11
- data/lib/action_view/vendor/html-scanner.rb +0 -20
@@ -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
|
@@ -0,0 +1,102 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/core_ext/enumerable"
|
4
|
+
|
5
|
+
module ActionView
|
6
|
+
module CollectionCaching # :nodoc:
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
included do
|
10
|
+
# Fallback cache store if Action View is used without Rails.
|
11
|
+
# Otherwise overridden in Railtie to use Rails.cache.
|
12
|
+
mattr_accessor :collection_cache, default: ActiveSupport::Cache::MemoryStore.new
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
def will_cache?(options, view)
|
17
|
+
options[:cached] && view.controller.respond_to?(:perform_caching) && view.controller.perform_caching
|
18
|
+
end
|
19
|
+
|
20
|
+
def cache_collection_render(instrumentation_payload, view, template, collection)
|
21
|
+
return yield(collection) unless will_cache?(@options, view)
|
22
|
+
|
23
|
+
collection_iterator = collection
|
24
|
+
|
25
|
+
# Result is a hash with the key represents the
|
26
|
+
# key used for cache lookup and the value is the item
|
27
|
+
# on which the partial is being rendered
|
28
|
+
keyed_collection, ordered_keys = collection_by_cache_keys(view, template, collection)
|
29
|
+
|
30
|
+
# Pull all partials from cache
|
31
|
+
# Result is a hash, key matches the entry in
|
32
|
+
# `keyed_collection` where the cache was retrieved and the
|
33
|
+
# value is the value that was present in the cache
|
34
|
+
cached_partials = collection_cache.read_multi(*keyed_collection.keys)
|
35
|
+
instrumentation_payload[:cache_hits] = cached_partials.size
|
36
|
+
|
37
|
+
# Extract the items for the keys that are not found
|
38
|
+
collection = keyed_collection.reject { |key, _| cached_partials.key?(key) }.values
|
39
|
+
|
40
|
+
rendered_partials = collection.empty? ? [] : yield(collection_iterator.from_collection(collection))
|
41
|
+
|
42
|
+
index = 0
|
43
|
+
keyed_partials = fetch_or_cache_partial(cached_partials, template, order_by: keyed_collection.each_key) do
|
44
|
+
# This block is called once
|
45
|
+
# for every cache miss while preserving order.
|
46
|
+
rendered_partials[index].tap { index += 1 }
|
47
|
+
end
|
48
|
+
|
49
|
+
ordered_keys.map do |key|
|
50
|
+
keyed_partials[key]
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def callable_cache_key?
|
55
|
+
@options[:cached].respond_to?(:call)
|
56
|
+
end
|
57
|
+
|
58
|
+
def collection_by_cache_keys(view, template, collection)
|
59
|
+
seed = callable_cache_key? ? @options[:cached] : ->(i) { i }
|
60
|
+
|
61
|
+
digest_path = view.digest_path_from_template(template)
|
62
|
+
|
63
|
+
collection.each_with_object([{}, []]) do |item, (hash, ordered_keys)|
|
64
|
+
key = expanded_cache_key(seed.call(item), view, template, digest_path)
|
65
|
+
ordered_keys << key
|
66
|
+
hash[key] = item
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def expanded_cache_key(key, view, template, digest_path)
|
71
|
+
key = view.combined_fragment_cache_key(view.cache_fragment_name(key, digest_path: digest_path))
|
72
|
+
key.frozen? ? key.dup : key # #read_multi & #write may require mutability, Dalli 2.6.0.
|
73
|
+
end
|
74
|
+
|
75
|
+
# `order_by` is an enumerable object containing keys of the cache,
|
76
|
+
# all keys are passed in whether found already or not.
|
77
|
+
#
|
78
|
+
# `cached_partials` is a hash. If the value exists
|
79
|
+
# it represents the rendered partial from the cache
|
80
|
+
# otherwise `Hash#fetch` will take the value of its block.
|
81
|
+
#
|
82
|
+
# This method expects a block that will return the rendered
|
83
|
+
# partial. An example is to render all results
|
84
|
+
# for each element that was not found in the cache and store it as an array.
|
85
|
+
# Order it so that the first empty cache element in `cached_partials`
|
86
|
+
# corresponds to the first element in `rendered_partials`.
|
87
|
+
#
|
88
|
+
# If the partial is not already cached it will also be
|
89
|
+
# written back to the underlying cache store.
|
90
|
+
def fetch_or_cache_partial(cached_partials, template, order_by:)
|
91
|
+
order_by.index_with do |cache_key|
|
92
|
+
if content = cached_partials[cache_key]
|
93
|
+
build_rendered_template(content, template)
|
94
|
+
else
|
95
|
+
yield.tap do |rendered_partial|
|
96
|
+
collection_cache.write(cache_key, rendered_partial.body)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -1,4 +1,6 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "action_view/renderer/partial_renderer/collection_caching"
|
2
4
|
|
3
5
|
module ActionView
|
4
6
|
# = Action View Partials
|
@@ -22,12 +24,12 @@ module ActionView
|
|
22
24
|
# <%= render partial: "ad", locals: { ad: ad } %>
|
23
25
|
# <% end %>
|
24
26
|
#
|
25
|
-
# This would first render
|
26
|
-
# render
|
27
|
+
# This would first render <tt>advertiser/_account.html.erb</tt> with <tt>@buyer</tt> passed in as the local variable +account+, then
|
28
|
+
# render <tt>advertiser/_ad.html.erb</tt> and pass the local variable +ad+ to the template for display.
|
27
29
|
#
|
28
30
|
# == The :as and :object options
|
29
31
|
#
|
30
|
-
# By default
|
32
|
+
# By default ActionView::PartialRenderer doesn't have any local variables.
|
31
33
|
# The <tt>:object</tt> option can be used to pass an object to the partial. For instance:
|
32
34
|
#
|
33
35
|
# <%= render partial: "account", object: @buyer %>
|
@@ -46,7 +48,7 @@ module ActionView
|
|
46
48
|
#
|
47
49
|
# <%= render partial: "account", locals: { user: @buyer } %>
|
48
50
|
#
|
49
|
-
# == Rendering a collection of partials
|
51
|
+
# == \Rendering a collection of partials
|
50
52
|
#
|
51
53
|
# The example of partial use describes a familiar pattern where a template needs to iterate over an array and
|
52
54
|
# render a sub template for each of the elements. This pattern has been implemented as a single method that
|
@@ -55,9 +57,13 @@ module ActionView
|
|
55
57
|
#
|
56
58
|
# <%= render partial: "ad", collection: @advertisements %>
|
57
59
|
#
|
58
|
-
# This will render
|
59
|
-
# iteration
|
60
|
-
# +
|
60
|
+
# This will render <tt>advertiser/_ad.html.erb</tt> and pass the local variable +ad+ to the template for display. An
|
61
|
+
# iteration object will automatically be made available to the template with a name of the form
|
62
|
+
# +partial_name_iteration+. The iteration object has knowledge about which index the current object has in
|
63
|
+
# the collection and the total size of the collection. The iteration object also has two convenience methods,
|
64
|
+
# +first?+ and +last?+. In the case of the example above, the template would be fed +ad_iteration+.
|
65
|
+
# For backwards compatibility the +partial_name_counter+ is still present and is mapped to the iteration's
|
66
|
+
# +index+ method.
|
61
67
|
#
|
62
68
|
# The <tt>:as</tt> option may be used when rendering partials.
|
63
69
|
#
|
@@ -66,37 +72,34 @@ module ActionView
|
|
66
72
|
#
|
67
73
|
# <%= render partial: "ad", collection: @advertisements, spacer_template: "ad_divider" %>
|
68
74
|
#
|
69
|
-
# If the given <tt>:collection</tt> is nil or empty, <tt>render</tt> will return nil
|
70
|
-
# to specify a text which will displayed instead by using this form:
|
75
|
+
# If the given <tt>:collection</tt> is +nil+ or empty, <tt>render</tt> will return +nil+. This will allow you
|
76
|
+
# to specify a text which will be displayed instead by using this form:
|
71
77
|
#
|
72
78
|
# <%= render(partial: "ad", collection: @advertisements) || "There's no ad to be displayed" %>
|
73
79
|
#
|
74
|
-
#
|
75
|
-
# just keep domain objects, like Active Records, in there.
|
76
|
-
#
|
77
|
-
# == Rendering shared partials
|
80
|
+
# == \Rendering shared partials
|
78
81
|
#
|
79
82
|
# Two controllers can share a set of partials and render them like this:
|
80
83
|
#
|
81
84
|
# <%= render partial: "advertisement/ad", locals: { ad: @advertisement } %>
|
82
85
|
#
|
83
|
-
# This will render the partial
|
86
|
+
# This will render the partial <tt>advertisement/_ad.html.erb</tt> regardless of which controller this is being called from.
|
84
87
|
#
|
85
|
-
# == Rendering objects that respond to
|
88
|
+
# == \Rendering objects that respond to +to_partial_path+
|
86
89
|
#
|
87
90
|
# Instead of explicitly naming the location of a partial, you can also let PartialRenderer do the work
|
88
|
-
# and pick the proper path by checking
|
91
|
+
# and pick the proper path by checking +to_partial_path+ method.
|
89
92
|
#
|
90
93
|
# # @account.to_partial_path returns 'accounts/account', so it can be used to replace:
|
91
94
|
# # <%= render partial: "accounts/account", locals: { account: @account} %>
|
92
95
|
# <%= render partial: @account %>
|
93
96
|
#
|
94
|
-
# # @posts is an array of Post instances, so every post record returns 'posts/post' on
|
97
|
+
# # @posts is an array of Post instances, so every post record returns 'posts/post' on +to_partial_path+,
|
95
98
|
# # that's why we can replace:
|
96
99
|
# # <%= render partial: "posts/post", collection: @posts %>
|
97
100
|
# <%= render partial: @posts %>
|
98
101
|
#
|
99
|
-
# == Rendering the default case
|
102
|
+
# == \Rendering the default case
|
100
103
|
#
|
101
104
|
# If you're not going to be using any of the options like collections or layouts, you can also use the short-hand
|
102
105
|
# defaults of render to render partials. Examples:
|
@@ -111,34 +114,34 @@ module ActionView
|
|
111
114
|
# # <%= render partial: "accounts/account", locals: { account: @account} %>
|
112
115
|
# <%= render @account %>
|
113
116
|
#
|
114
|
-
# # @posts is an array of Post instances, so every post record returns 'posts/post' on
|
117
|
+
# # @posts is an array of Post instances, so every post record returns 'posts/post' on +to_partial_path+,
|
115
118
|
# # that's why we can replace:
|
116
119
|
# # <%= render partial: "posts/post", collection: @posts %>
|
117
120
|
# <%= render @posts %>
|
118
121
|
#
|
119
|
-
# == Rendering partials with layouts
|
122
|
+
# == \Rendering partials with layouts
|
120
123
|
#
|
121
124
|
# Partials can have their own layouts applied to them. These layouts are different than the ones that are
|
122
125
|
# specified globally for the entire action, but they work in a similar fashion. Imagine a list with two types
|
123
126
|
# of users:
|
124
127
|
#
|
125
|
-
# <%# app/views/users/index.html.erb
|
128
|
+
# <%# app/views/users/index.html.erb %>
|
126
129
|
# Here's the administrator:
|
127
130
|
# <%= render partial: "user", layout: "administrator", locals: { user: administrator } %>
|
128
131
|
#
|
129
132
|
# Here's the editor:
|
130
133
|
# <%= render partial: "user", layout: "editor", locals: { user: editor } %>
|
131
134
|
#
|
132
|
-
# <%# app/views/users/_user.html.erb
|
135
|
+
# <%# app/views/users/_user.html.erb %>
|
133
136
|
# Name: <%= user.name %>
|
134
137
|
#
|
135
|
-
# <%# app/views/users/_administrator.html.erb
|
138
|
+
# <%# app/views/users/_administrator.html.erb %>
|
136
139
|
# <div id="administrator">
|
137
140
|
# Budget: $<%= user.budget %>
|
138
141
|
# <%= yield %>
|
139
142
|
# </div>
|
140
143
|
#
|
141
|
-
# <%# app/views/users/_editor.html.erb
|
144
|
+
# <%# app/views/users/_editor.html.erb %>
|
142
145
|
# <div id="editor">
|
143
146
|
# Deadline: <%= user.deadline %>
|
144
147
|
# <%= yield %>
|
@@ -201,7 +204,7 @@ module ActionView
|
|
201
204
|
#
|
202
205
|
# You can also apply a layout to a block within any template:
|
203
206
|
#
|
204
|
-
# <%# app/views/users/_chief.html.erb
|
207
|
+
# <%# app/views/users/_chief.html.erb %>
|
205
208
|
# <%= render(layout: "administrator", locals: { user: chief }) do %>
|
206
209
|
# Title: <%= chief.title %>
|
207
210
|
# <% end %>
|
@@ -218,13 +221,13 @@ module ActionView
|
|
218
221
|
# If you pass arguments to "yield" then this will be passed to the block. One way to use this is to pass
|
219
222
|
# an array to layout and treat it as an enumerable.
|
220
223
|
#
|
221
|
-
# <%# app/views/users/_user.html.erb
|
224
|
+
# <%# app/views/users/_user.html.erb %>
|
222
225
|
# <div class="user">
|
223
226
|
# Budget: $<%= user.budget %>
|
224
227
|
# <%= yield user %>
|
225
228
|
# </div>
|
226
229
|
#
|
227
|
-
# <%# app/views/users/index.html.erb
|
230
|
+
# <%# app/views/users/index.html.erb %>
|
228
231
|
# <%= render layout: @users do |user| %>
|
229
232
|
# Title: <%= user.title %>
|
230
233
|
# <% end %>
|
@@ -233,14 +236,14 @@ module ActionView
|
|
233
236
|
#
|
234
237
|
# You can also yield multiple times in one layout and use block arguments to differentiate the sections.
|
235
238
|
#
|
236
|
-
# <%# app/views/users/_user.html.erb
|
239
|
+
# <%# app/views/users/_user.html.erb %>
|
237
240
|
# <div class="user">
|
238
241
|
# <%= yield user, :header %>
|
239
242
|
# Budget: $<%= user.budget %>
|
240
243
|
# <%= yield user, :footer %>
|
241
244
|
# </div>
|
242
245
|
#
|
243
|
-
# <%# app/views/users/index.html.erb
|
246
|
+
# <%# app/views/users/index.html.erb %>
|
244
247
|
# <%= render layout: @users do |user, section| %>
|
245
248
|
# <%- case section when :header -%>
|
246
249
|
# Title: <%= user.title %>
|
@@ -249,252 +252,49 @@ module ActionView
|
|
249
252
|
# <%- end -%>
|
250
253
|
# <% end %>
|
251
254
|
class PartialRenderer < AbstractRenderer
|
252
|
-
|
253
|
-
h[k] = ThreadSafe::Cache.new
|
254
|
-
end
|
255
|
-
|
256
|
-
def initialize(*)
|
257
|
-
super
|
258
|
-
@context_prefix = @lookup_context.prefixes.first
|
259
|
-
end
|
260
|
-
|
261
|
-
def render(context, options, block)
|
262
|
-
setup(context, options, block)
|
263
|
-
identifier = (@template = find_partial) ? @template.identifier : @path
|
264
|
-
|
265
|
-
@lookup_context.rendered_format ||= begin
|
266
|
-
if @template && @template.formats.present?
|
267
|
-
@template.formats.first
|
268
|
-
else
|
269
|
-
formats.first
|
270
|
-
end
|
271
|
-
end
|
272
|
-
|
273
|
-
if @collection
|
274
|
-
instrument(:collection, :identifier => identifier || "collection", :count => @collection.size) do
|
275
|
-
render_collection
|
276
|
-
end
|
277
|
-
else
|
278
|
-
instrument(:partial, :identifier => identifier) do
|
279
|
-
render_partial
|
280
|
-
end
|
281
|
-
end
|
282
|
-
end
|
283
|
-
|
284
|
-
def render_collection
|
285
|
-
return nil if @collection.blank?
|
255
|
+
include CollectionCaching
|
286
256
|
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
result.join(spacer).html_safe
|
257
|
+
def initialize(lookup_context, options)
|
258
|
+
super(lookup_context)
|
259
|
+
@options = options
|
260
|
+
@locals = @options[:locals] || {}
|
261
|
+
@details = extract_details(@options)
|
293
262
|
end
|
294
263
|
|
295
|
-
def
|
296
|
-
|
297
|
-
object, as = @object, @variable
|
264
|
+
def render(partial, context, block)
|
265
|
+
template = find_template(partial, template_keys(partial))
|
298
266
|
|
299
267
|
if !block && (layout = @options[:layout])
|
300
|
-
layout = find_template(layout.to_s,
|
301
|
-
end
|
302
|
-
|
303
|
-
object ||= locals[as]
|
304
|
-
locals[as] = object
|
305
|
-
|
306
|
-
content = @template.render(view, locals) do |*name|
|
307
|
-
view._layout_for(*name, &block)
|
268
|
+
layout = find_template(layout.to_s, template_keys(partial))
|
308
269
|
end
|
309
270
|
|
310
|
-
|
311
|
-
content
|
271
|
+
render_partial_template(context, @locals, template, layout, block)
|
312
272
|
end
|
313
273
|
|
314
274
|
private
|
315
|
-
|
316
|
-
|
317
|
-
# finds the options and details and extracts them. The method also contains
|
318
|
-
# logic that handles the type of object passed in as the partial.
|
319
|
-
#
|
320
|
-
# If +options[:partial]+ is a string, then the +@path+ instance variable is
|
321
|
-
# set to that string. Otherwise, the +options[:partial]+ object must
|
322
|
-
# respond to +to_partial_path+ in order to setup the path.
|
323
|
-
def setup(context, options, block)
|
324
|
-
@view = context
|
325
|
-
partial = options[:partial]
|
326
|
-
|
327
|
-
@options = options
|
328
|
-
@locals = options[:locals] || {}
|
329
|
-
@block = block
|
330
|
-
@details = extract_details(options)
|
331
|
-
|
332
|
-
prepend_formats(options[:formats])
|
333
|
-
|
334
|
-
if String === partial
|
335
|
-
@object = options[:object] if options.has_key?(:object)
|
336
|
-
@path = partial
|
337
|
-
@collection = collection
|
338
|
-
else
|
339
|
-
@object = partial
|
340
|
-
|
341
|
-
if @collection = collection_from_object || collection
|
342
|
-
paths = @collection_data = @collection.map { |o| partial_path(o) }
|
343
|
-
@path = paths.uniq.size == 1 ? paths.first : nil
|
344
|
-
else
|
345
|
-
@path = partial_path
|
346
|
-
end
|
347
|
-
end
|
348
|
-
|
349
|
-
if as = options[:as]
|
350
|
-
raise_invalid_option_as(as) unless as.to_s =~ /\A[a-z_]\w*\z/
|
351
|
-
as = as.to_sym
|
352
|
-
end
|
353
|
-
|
354
|
-
if @path
|
355
|
-
@variable, @variable_counter = retrieve_variable(@path, as)
|
356
|
-
@template_keys = retrieve_template_keys
|
357
|
-
else
|
358
|
-
paths.map! { |path| retrieve_variable(path, as).unshift(path) }
|
275
|
+
def template_keys(_)
|
276
|
+
@locals.keys
|
359
277
|
end
|
360
278
|
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
def find_partial
|
376
|
-
if path = @path
|
377
|
-
find_template(path, @template_keys)
|
378
|
-
end
|
379
|
-
end
|
380
|
-
|
381
|
-
def find_template(path, locals)
|
382
|
-
prefixes = path.include?(?/) ? [] : @lookup_context.prefixes
|
383
|
-
@lookup_context.find_template(path, prefixes, true, locals, @details)
|
384
|
-
end
|
385
|
-
|
386
|
-
def collection_with_template
|
387
|
-
view, locals, template = @view, @locals, @template
|
388
|
-
as, counter = @variable, @variable_counter
|
389
|
-
|
390
|
-
if layout = @options[:layout]
|
391
|
-
layout = find_template(layout, @template_keys)
|
392
|
-
end
|
393
|
-
|
394
|
-
index = -1
|
395
|
-
@collection.map do |object|
|
396
|
-
locals[as] = object
|
397
|
-
locals[counter] = (index += 1)
|
398
|
-
|
399
|
-
content = template.render(view, locals)
|
400
|
-
content = layout.render(view, locals) { content } if layout
|
401
|
-
content
|
402
|
-
end
|
403
|
-
end
|
404
|
-
|
405
|
-
def collection_without_template
|
406
|
-
view, locals, collection_data = @view, @locals, @collection_data
|
407
|
-
cache = {}
|
408
|
-
keys = @locals.keys
|
409
|
-
|
410
|
-
index = -1
|
411
|
-
@collection.map do |object|
|
412
|
-
index += 1
|
413
|
-
path, as, counter = collection_data[index]
|
414
|
-
|
415
|
-
locals[as] = object
|
416
|
-
locals[counter] = index
|
417
|
-
|
418
|
-
template = (cache[path] ||= find_template(path, keys + [as, counter]))
|
419
|
-
template.render(view, locals)
|
420
|
-
end
|
421
|
-
end
|
422
|
-
|
423
|
-
# Obtains the path to where the object's partial is located. If the object
|
424
|
-
# responds to +to_partial_path+, then +to_partial_path+ will be called and
|
425
|
-
# will provide the path. If the object does not respond to +to_partial_path+,
|
426
|
-
# then an +ArgumentError+ is raised.
|
427
|
-
#
|
428
|
-
# If +prefix_partial_path_with_controller_namespace+ is true, then this
|
429
|
-
# method will prefix the partial paths with a namespace.
|
430
|
-
def partial_path(object = @object)
|
431
|
-
object = object.to_model if object.respond_to?(:to_model)
|
432
|
-
|
433
|
-
path = if object.respond_to?(:to_partial_path)
|
434
|
-
object.to_partial_path
|
435
|
-
else
|
436
|
-
raise ArgumentError.new("'#{object.inspect}' is not an ActiveModel-compatible object. It must implement :to_partial_path.")
|
437
|
-
end
|
438
|
-
|
439
|
-
if @view.prefix_partial_path_with_controller_namespace
|
440
|
-
prefixed_partial_names[path] ||= merge_prefix_into_object_path(@context_prefix, path.dup)
|
441
|
-
else
|
442
|
-
path
|
443
|
-
end
|
444
|
-
end
|
445
|
-
|
446
|
-
def prefixed_partial_names
|
447
|
-
@prefixed_partial_names ||= PREFIXED_PARTIAL_NAMES[@context_prefix]
|
448
|
-
end
|
449
|
-
|
450
|
-
def merge_prefix_into_object_path(prefix, object_path)
|
451
|
-
if prefix.include?(?/) && object_path.include?(?/)
|
452
|
-
prefixes = []
|
453
|
-
prefix_array = File.dirname(prefix).split('/')
|
454
|
-
object_path_array = object_path.split('/')[0..-3] # skip model dir & partial
|
455
|
-
|
456
|
-
prefix_array.each_with_index do |dir, index|
|
457
|
-
break if dir == object_path_array[index]
|
458
|
-
prefixes << dir
|
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|
|
286
|
+
view._layout_for(*name, &block)
|
287
|
+
end
|
288
|
+
|
289
|
+
content = layout.render(view, locals) { content } if layout
|
290
|
+
payload[:cache_hit] = view.view_renderer.cache_hits[template.virtual_path]
|
291
|
+
build_rendered_template(content, template)
|
459
292
|
end
|
460
|
-
|
461
|
-
(prefixes << object_path).join("/")
|
462
|
-
else
|
463
|
-
object_path
|
464
293
|
end
|
465
|
-
end
|
466
|
-
|
467
|
-
def retrieve_template_keys
|
468
|
-
keys = @locals.keys
|
469
|
-
keys << @variable if defined?(@object) || @collection
|
470
|
-
keys << @variable_counter if @collection
|
471
|
-
keys
|
472
|
-
end
|
473
294
|
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
raise_invalid_identifier(path) unless base =~ /\A_?([a-z]\w*)(\.\w+)*\z/
|
478
|
-
$1.to_sym
|
295
|
+
def find_template(path, locals)
|
296
|
+
prefixes = path.include?(?/) ? [] : @lookup_context.prefixes
|
297
|
+
@lookup_context.find_template(path, prefixes, true, locals, @details)
|
479
298
|
end
|
480
|
-
variable_counter = :"#{variable}_counter" if @collection
|
481
|
-
[variable, variable_counter]
|
482
|
-
end
|
483
|
-
|
484
|
-
IDENTIFIER_ERROR_MESSAGE = "The partial name (%s) is not a valid Ruby identifier; " +
|
485
|
-
"make sure your partial name starts with underscore, " +
|
486
|
-
"and is followed by any combination of letters, numbers and underscores."
|
487
|
-
|
488
|
-
OPTION_AS_ERROR_MESSAGE = "The value (%s) of the option `as` is not a valid Ruby identifier; " +
|
489
|
-
"make sure it starts with lowercase letter, " +
|
490
|
-
"and is followed by any combination of letters, numbers and underscores."
|
491
|
-
|
492
|
-
def raise_invalid_identifier(path)
|
493
|
-
raise ArgumentError.new(IDENTIFIER_ERROR_MESSAGE % (path))
|
494
|
-
end
|
495
|
-
|
496
|
-
def raise_invalid_option_as(as)
|
497
|
-
raise ArgumentError.new(OPTION_AS_ERROR_MESSAGE % (as))
|
498
|
-
end
|
499
299
|
end
|
500
300
|
end
|