omg-actionview 8.0.0.alpha1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (130) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +25 -0
  3. data/MIT-LICENSE +21 -0
  4. data/README.rdoc +40 -0
  5. data/app/assets/javascripts/rails-ujs.esm.js +686 -0
  6. data/app/assets/javascripts/rails-ujs.js +630 -0
  7. data/lib/action_view/base.rb +316 -0
  8. data/lib/action_view/buffers.rb +165 -0
  9. data/lib/action_view/cache_expiry.rb +69 -0
  10. data/lib/action_view/context.rb +32 -0
  11. data/lib/action_view/dependency_tracker/erb_tracker.rb +159 -0
  12. data/lib/action_view/dependency_tracker/ruby_tracker.rb +43 -0
  13. data/lib/action_view/dependency_tracker/wildcard_resolver.rb +32 -0
  14. data/lib/action_view/dependency_tracker.rb +41 -0
  15. data/lib/action_view/deprecator.rb +7 -0
  16. data/lib/action_view/digestor.rb +130 -0
  17. data/lib/action_view/flows.rb +75 -0
  18. data/lib/action_view/gem_version.rb +17 -0
  19. data/lib/action_view/helpers/active_model_helper.rb +54 -0
  20. data/lib/action_view/helpers/asset_tag_helper.rb +680 -0
  21. data/lib/action_view/helpers/asset_url_helper.rb +473 -0
  22. data/lib/action_view/helpers/atom_feed_helper.rb +205 -0
  23. data/lib/action_view/helpers/cache_helper.rb +315 -0
  24. data/lib/action_view/helpers/capture_helper.rb +236 -0
  25. data/lib/action_view/helpers/content_exfiltration_prevention_helper.rb +70 -0
  26. data/lib/action_view/helpers/controller_helper.rb +42 -0
  27. data/lib/action_view/helpers/csp_helper.rb +26 -0
  28. data/lib/action_view/helpers/csrf_helper.rb +35 -0
  29. data/lib/action_view/helpers/date_helper.rb +1266 -0
  30. data/lib/action_view/helpers/debug_helper.rb +38 -0
  31. data/lib/action_view/helpers/form_helper.rb +2765 -0
  32. data/lib/action_view/helpers/form_options_helper.rb +927 -0
  33. data/lib/action_view/helpers/form_tag_helper.rb +1088 -0
  34. data/lib/action_view/helpers/javascript_helper.rb +96 -0
  35. data/lib/action_view/helpers/number_helper.rb +165 -0
  36. data/lib/action_view/helpers/output_safety_helper.rb +70 -0
  37. data/lib/action_view/helpers/rendering_helper.rb +218 -0
  38. data/lib/action_view/helpers/sanitize_helper.rb +201 -0
  39. data/lib/action_view/helpers/tag_helper.rb +621 -0
  40. data/lib/action_view/helpers/tags/base.rb +138 -0
  41. data/lib/action_view/helpers/tags/check_box.rb +65 -0
  42. data/lib/action_view/helpers/tags/checkable.rb +18 -0
  43. data/lib/action_view/helpers/tags/collection_check_boxes.rb +37 -0
  44. data/lib/action_view/helpers/tags/collection_helpers.rb +118 -0
  45. data/lib/action_view/helpers/tags/collection_radio_buttons.rb +31 -0
  46. data/lib/action_view/helpers/tags/collection_select.rb +33 -0
  47. data/lib/action_view/helpers/tags/color_field.rb +26 -0
  48. data/lib/action_view/helpers/tags/date_field.rb +14 -0
  49. data/lib/action_view/helpers/tags/date_select.rb +75 -0
  50. data/lib/action_view/helpers/tags/datetime_field.rb +39 -0
  51. data/lib/action_view/helpers/tags/datetime_local_field.rb +29 -0
  52. data/lib/action_view/helpers/tags/datetime_select.rb +10 -0
  53. data/lib/action_view/helpers/tags/email_field.rb +10 -0
  54. data/lib/action_view/helpers/tags/file_field.rb +26 -0
  55. data/lib/action_view/helpers/tags/grouped_collection_select.rb +34 -0
  56. data/lib/action_view/helpers/tags/hidden_field.rb +14 -0
  57. data/lib/action_view/helpers/tags/label.rb +84 -0
  58. data/lib/action_view/helpers/tags/month_field.rb +14 -0
  59. data/lib/action_view/helpers/tags/number_field.rb +20 -0
  60. data/lib/action_view/helpers/tags/password_field.rb +14 -0
  61. data/lib/action_view/helpers/tags/placeholderable.rb +24 -0
  62. data/lib/action_view/helpers/tags/radio_button.rb +32 -0
  63. data/lib/action_view/helpers/tags/range_field.rb +10 -0
  64. data/lib/action_view/helpers/tags/search_field.rb +27 -0
  65. data/lib/action_view/helpers/tags/select.rb +45 -0
  66. data/lib/action_view/helpers/tags/select_renderer.rb +56 -0
  67. data/lib/action_view/helpers/tags/tel_field.rb +10 -0
  68. data/lib/action_view/helpers/tags/text_area.rb +24 -0
  69. data/lib/action_view/helpers/tags/text_field.rb +33 -0
  70. data/lib/action_view/helpers/tags/time_field.rb +23 -0
  71. data/lib/action_view/helpers/tags/time_select.rb +10 -0
  72. data/lib/action_view/helpers/tags/time_zone_select.rb +25 -0
  73. data/lib/action_view/helpers/tags/translator.rb +39 -0
  74. data/lib/action_view/helpers/tags/url_field.rb +10 -0
  75. data/lib/action_view/helpers/tags/week_field.rb +14 -0
  76. data/lib/action_view/helpers/tags/weekday_select.rb +31 -0
  77. data/lib/action_view/helpers/tags.rb +47 -0
  78. data/lib/action_view/helpers/text_helper.rb +568 -0
  79. data/lib/action_view/helpers/translation_helper.rb +161 -0
  80. data/lib/action_view/helpers/url_helper.rb +812 -0
  81. data/lib/action_view/helpers.rb +68 -0
  82. data/lib/action_view/layouts.rb +434 -0
  83. data/lib/action_view/locale/en.yml +56 -0
  84. data/lib/action_view/log_subscriber.rb +132 -0
  85. data/lib/action_view/lookup_context.rb +299 -0
  86. data/lib/action_view/model_naming.rb +14 -0
  87. data/lib/action_view/path_registry.rb +57 -0
  88. data/lib/action_view/path_set.rb +84 -0
  89. data/lib/action_view/railtie.rb +132 -0
  90. data/lib/action_view/record_identifier.rb +118 -0
  91. data/lib/action_view/render_parser/prism_render_parser.rb +139 -0
  92. data/lib/action_view/render_parser/ripper_render_parser.rb +350 -0
  93. data/lib/action_view/render_parser.rb +40 -0
  94. data/lib/action_view/renderer/abstract_renderer.rb +186 -0
  95. data/lib/action_view/renderer/collection_renderer.rb +204 -0
  96. data/lib/action_view/renderer/object_renderer.rb +34 -0
  97. data/lib/action_view/renderer/partial_renderer/collection_caching.rb +120 -0
  98. data/lib/action_view/renderer/partial_renderer.rb +267 -0
  99. data/lib/action_view/renderer/renderer.rb +107 -0
  100. data/lib/action_view/renderer/streaming_template_renderer.rb +107 -0
  101. data/lib/action_view/renderer/template_renderer.rb +115 -0
  102. data/lib/action_view/rendering.rb +190 -0
  103. data/lib/action_view/routing_url_for.rb +149 -0
  104. data/lib/action_view/tasks/cache_digests.rake +25 -0
  105. data/lib/action_view/template/error.rb +264 -0
  106. data/lib/action_view/template/handlers/builder.rb +25 -0
  107. data/lib/action_view/template/handlers/erb/erubi.rb +85 -0
  108. data/lib/action_view/template/handlers/erb.rb +157 -0
  109. data/lib/action_view/template/handlers/html.rb +11 -0
  110. data/lib/action_view/template/handlers/raw.rb +11 -0
  111. data/lib/action_view/template/handlers.rb +66 -0
  112. data/lib/action_view/template/html.rb +33 -0
  113. data/lib/action_view/template/inline.rb +22 -0
  114. data/lib/action_view/template/raw_file.rb +25 -0
  115. data/lib/action_view/template/renderable.rb +30 -0
  116. data/lib/action_view/template/resolver.rb +212 -0
  117. data/lib/action_view/template/sources/file.rb +17 -0
  118. data/lib/action_view/template/sources.rb +13 -0
  119. data/lib/action_view/template/text.rb +32 -0
  120. data/lib/action_view/template/types.rb +50 -0
  121. data/lib/action_view/template.rb +580 -0
  122. data/lib/action_view/template_details.rb +66 -0
  123. data/lib/action_view/template_path.rb +66 -0
  124. data/lib/action_view/test_case.rb +449 -0
  125. data/lib/action_view/testing/resolvers.rb +44 -0
  126. data/lib/action_view/unbound_template.rb +67 -0
  127. data/lib/action_view/version.rb +10 -0
  128. data/lib/action_view/view_paths.rb +117 -0
  129. data/lib/action_view.rb +104 -0
  130. metadata +275 -0
@@ -0,0 +1,204 @@
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
+
55
+ def preload!
56
+ # no-op
57
+ end
58
+ end
59
+
60
+ class SameCollectionIterator < CollectionIterator # :nodoc:
61
+ def initialize(collection, path, variables)
62
+ super(collection)
63
+ @path = path
64
+ @variables = variables
65
+ end
66
+
67
+ def from_collection(collection)
68
+ self.class.new(collection, @path, @variables)
69
+ end
70
+
71
+ def each_with_info
72
+ return enum_for(:each_with_info) unless block_given?
73
+ variables = [@path] + @variables
74
+ @collection.each { |o| yield(o, variables) }
75
+ end
76
+ end
77
+
78
+ class PreloadCollectionIterator < SameCollectionIterator # :nodoc:
79
+ def initialize(collection, path, variables, relation)
80
+ super(collection, path, variables)
81
+ relation.skip_preloading! unless relation.loaded?
82
+ @relation = relation
83
+ end
84
+
85
+ def from_collection(collection)
86
+ self.class.new(collection, @path, @variables, @relation)
87
+ end
88
+
89
+ def each_with_info
90
+ return super unless block_given?
91
+ preload!
92
+ super
93
+ end
94
+
95
+ def preload!
96
+ @relation.preload_associations(@collection)
97
+ end
98
+ end
99
+
100
+ class MixedCollectionIterator < CollectionIterator # :nodoc:
101
+ def initialize(collection, paths)
102
+ super(collection)
103
+ @paths = paths
104
+ end
105
+
106
+ def each_with_info
107
+ return enum_for(:each_with_info) unless block_given?
108
+ @collection.each_with_index { |o, i| yield(o, @paths[i]) }
109
+ end
110
+ end
111
+
112
+ def render_collection_with_partial(collection, partial, context, block)
113
+ iter_vars = retrieve_variable(partial)
114
+
115
+ collection = if collection.respond_to?(:preload_associations)
116
+ PreloadCollectionIterator.new(collection, partial, iter_vars, collection)
117
+ else
118
+ SameCollectionIterator.new(collection, partial, iter_vars)
119
+ end
120
+
121
+ template = find_template(partial, @locals.keys + iter_vars)
122
+
123
+ layout = if !block && (layout = @options[:layout])
124
+ find_template(layout.to_s, @locals.keys + iter_vars)
125
+ end
126
+
127
+ render_collection(collection, context, partial, template, layout, block)
128
+ end
129
+
130
+ def render_collection_derive_partial(collection, context, block)
131
+ paths = collection.map { |o| partial_path(o, context) }
132
+
133
+ if paths.uniq.length == 1
134
+ # Homogeneous
135
+ render_collection_with_partial(collection, paths.first, context, block)
136
+ else
137
+ if @options[:cached]
138
+ raise NotImplementedError, "render caching requires a template. Please specify a partial when rendering"
139
+ end
140
+
141
+ paths.map! { |path| retrieve_variable(path).unshift(path) }
142
+ collection = MixedCollectionIterator.new(collection, paths)
143
+ render_collection(collection, context, nil, nil, nil, block)
144
+ end
145
+ end
146
+
147
+ private
148
+ def retrieve_variable(path)
149
+ variable = local_variable(path)
150
+ [variable, :"#{variable}_counter", :"#{variable}_iteration"]
151
+ end
152
+
153
+ def render_collection(collection, view, path, template, layout, block)
154
+ identifier = (template && template.identifier) || path
155
+ ActiveSupport::Notifications.instrument(
156
+ "render_collection.action_view",
157
+ identifier: identifier,
158
+ layout: layout && layout.virtual_path,
159
+ count: collection.length
160
+ ) do |payload|
161
+ spacer = if @options.key?(:spacer_template)
162
+ spacer_template = find_template(@options[:spacer_template], @locals.keys)
163
+ build_rendered_template(spacer_template.render(view, @locals), spacer_template)
164
+ else
165
+ RenderedTemplate::EMPTY_SPACER
166
+ end
167
+
168
+ collection_body = if template
169
+ cache_collection_render(payload, view, template, collection) do |filtered_collection|
170
+ collection_with_template(view, template, layout, filtered_collection)
171
+ end
172
+ else
173
+ collection_with_template(view, nil, layout, collection)
174
+ end
175
+
176
+ return RenderedCollection.empty(@lookup_context.formats.first) if collection_body.empty?
177
+
178
+ build_rendered_collection(collection_body, spacer)
179
+ end
180
+ end
181
+
182
+ def collection_with_template(view, template, layout, collection)
183
+ locals = @locals
184
+ cache = {}
185
+
186
+ partial_iteration = PartialIteration.new(collection.size)
187
+
188
+ collection.each_with_info.map do |object, (path, as, counter, iteration)|
189
+ index = partial_iteration.index
190
+
191
+ locals[as] = object
192
+ locals[counter] = index
193
+ locals[iteration] = partial_iteration
194
+
195
+ _template = (cache[path] ||= (template || find_template(path, @locals.keys + [as, counter, iteration])))
196
+
197
+ content = _template.render(view, locals, implicit_locals: [counter, iteration])
198
+ content = layout.render(view, locals) { content } if layout
199
+ partial_iteration.iterate!
200
+ build_rendered_template(content, _template)
201
+ end
202
+ end
203
+ end
204
+ 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,120 @@
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
+ collection.preload! if callable_cache_key?
63
+
64
+ collection.each_with_object([{}, []]) do |item, (hash, ordered_keys)|
65
+ key = expanded_cache_key(seed.call(item), view, template, digest_path)
66
+ ordered_keys << key
67
+ hash[key] = item
68
+ end
69
+ end
70
+
71
+ def expanded_cache_key(key, view, template, digest_path)
72
+ key = view.combined_fragment_cache_key(view.cache_fragment_name(key, digest_path: digest_path))
73
+ key.frozen? ? key.dup : key # #read_multi & #write may require mutability, Dalli 2.6.0.
74
+ end
75
+
76
+ # `order_by` is an enumerable object containing keys of the cache,
77
+ # all keys are passed in whether found already or not.
78
+ #
79
+ # `cached_partials` is a hash. If the value exists
80
+ # it represents the rendered partial from the cache
81
+ # otherwise `Hash#fetch` will take the value of its block.
82
+ #
83
+ # This method expects a block that will return the rendered
84
+ # partial. An example is to render all results
85
+ # for each element that was not found in the cache and store it as an array.
86
+ # Order it so that the first empty cache element in `cached_partials`
87
+ # corresponds to the first element in `rendered_partials`.
88
+ #
89
+ # If the partial is not already cached it will also be
90
+ # written back to the underlying cache store.
91
+ def fetch_or_cache_partial(cached_partials, template, order_by:)
92
+ entries_to_write = {}
93
+
94
+ keyed_partials = order_by.index_with do |cache_key|
95
+ if content = cached_partials[cache_key]
96
+ build_rendered_template(content, template)
97
+ else
98
+ rendered_partial = yield
99
+ body = rendered_partial.body
100
+
101
+ # We want to cache buffers as raw strings. This both improve performance and
102
+ # avoid creating forward compatibility issues with the internal representation
103
+ # of these two types.
104
+ if body.is_a?(ActionView::OutputBuffer) || body.is_a?(ActiveSupport::SafeBuffer)
105
+ body = body.to_str
106
+ end
107
+
108
+ entries_to_write[cache_key] = body
109
+ rendered_partial
110
+ end
111
+ end
112
+
113
+ unless entries_to_write.empty?
114
+ collection_cache.write_multi(entries_to_write)
115
+ end
116
+
117
+ keyed_partials
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,267 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "action_view/renderer/partial_renderer/collection_caching"
4
+
5
+ module ActionView
6
+ # = Action View Partials
7
+ #
8
+ # There's also a convenience method for rendering sub templates within the current controller that depends on a
9
+ # single object (we call this kind of sub templates for partials). It relies on the fact that partials should
10
+ # follow the naming convention of being prefixed with an underscore -- as to separate them from regular
11
+ # templates that could be rendered on their own.
12
+ #
13
+ # In a template for Advertiser#account:
14
+ #
15
+ # <%= render partial: "account" %>
16
+ #
17
+ # This would render "advertiser/_account.html.erb".
18
+ #
19
+ # In another template for Advertiser#buy, we could have:
20
+ #
21
+ # <%= render partial: "account", locals: { account: @buyer } %>
22
+ #
23
+ # <% @advertisements.each do |ad| %>
24
+ # <%= render partial: "ad", locals: { ad: ad } %>
25
+ # <% end %>
26
+ #
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.
29
+ #
30
+ # == The +:as+ and +:object+ options
31
+ #
32
+ # By default ActionView::PartialRenderer doesn't have any local variables.
33
+ # The <tt>:object</tt> option can be used to pass an object to the partial. For instance:
34
+ #
35
+ # <%= render partial: "account", object: @buyer %>
36
+ #
37
+ # would provide the <tt>@buyer</tt> object to the partial, available under the local variable +account+ and is
38
+ # equivalent to:
39
+ #
40
+ # <%= render partial: "account", locals: { account: @buyer } %>
41
+ #
42
+ # With the <tt>:as</tt> option we can specify a different name for said local variable. For example, if we
43
+ # wanted it to be +user+ instead of +account+ we'd do:
44
+ #
45
+ # <%= render partial: "account", object: @buyer, as: 'user' %>
46
+ #
47
+ # This is equivalent to
48
+ #
49
+ # <%= render partial: "account", locals: { user: @buyer } %>
50
+ #
51
+ # == \Rendering a collection of partials
52
+ #
53
+ # The example of partial use describes a familiar pattern where a template needs to iterate over an array and
54
+ # render a sub template for each of the elements. This pattern has been implemented as a single method that
55
+ # accepts an array and renders a partial by the same name as the elements contained within. So the three-lined
56
+ # example in "Using partials" can be rewritten with a single line:
57
+ #
58
+ # <%= render partial: "ad", collection: @advertisements %>
59
+ #
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.
67
+ #
68
+ # The <tt>:as</tt> option may be used when rendering partials.
69
+ #
70
+ # You can specify a partial to be rendered between elements via the <tt>:spacer_template</tt> option.
71
+ # The following example will render <tt>advertiser/_ad_divider.html.erb</tt> between each ad partial:
72
+ #
73
+ # <%= render partial: "ad", collection: @advertisements, spacer_template: "ad_divider" %>
74
+ #
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:
77
+ #
78
+ # <%= render(partial: "ad", collection: @advertisements) || "There's no ad to be displayed" %>
79
+ #
80
+ # == \Rendering shared partials
81
+ #
82
+ # Two controllers can share a set of partials and render them like this:
83
+ #
84
+ # <%= render partial: "advertisement/ad", locals: { ad: @advertisement } %>
85
+ #
86
+ # This will render the partial <tt>advertisement/_ad.html.erb</tt> regardless of which controller this is being called from.
87
+ #
88
+ # == \Rendering objects that respond to +to_partial_path+
89
+ #
90
+ # Instead of explicitly naming the location of a partial, you can also let PartialRenderer do the work
91
+ # and pick the proper path by checking +to_partial_path+ method.
92
+ #
93
+ # # @account.to_partial_path returns 'accounts/account', so it can be used to replace:
94
+ # # <%= render partial: "accounts/account", locals: { account: @account} %>
95
+ # <%= render partial: @account %>
96
+ #
97
+ # # @posts is an array of Post instances, so every post record returns 'posts/post' on +to_partial_path+,
98
+ # # that's why we can replace:
99
+ # # <%= render partial: "posts/post", collection: @posts %>
100
+ # <%= render partial: @posts %>
101
+ #
102
+ # == \Rendering the default case
103
+ #
104
+ # If you're not going to be using any of the options like collections or layouts, you can also use the short-hand
105
+ # defaults of render to render partials. Examples:
106
+ #
107
+ # # Instead of <%= render partial: "account" %>
108
+ # <%= render "account" %>
109
+ #
110
+ # # Instead of <%= render partial: "account", locals: { account: @buyer } %>
111
+ # <%= render "account", account: @buyer %>
112
+ #
113
+ # # @account.to_partial_path returns 'accounts/account', so it can be used to replace:
114
+ # # <%= render partial: "accounts/account", locals: { account: @account} %>
115
+ # <%= render @account %>
116
+ #
117
+ # # @posts is an array of Post instances, so every post record returns 'posts/post' on +to_partial_path+,
118
+ # # that's why we can replace:
119
+ # # <%= render partial: "posts/post", collection: @posts %>
120
+ # <%= render @posts %>
121
+ #
122
+ # == \Rendering partials with layouts
123
+ #
124
+ # Partials can have their own layouts applied to them. These layouts are different than the ones that are
125
+ # specified globally for the entire action, but they work in a similar fashion. Imagine a list with two types
126
+ # of users:
127
+ #
128
+ # <%# app/views/users/index.html.erb %>
129
+ # Here's the administrator:
130
+ # <%= render partial: "user", layout: "administrator", locals: { user: administrator } %>
131
+ #
132
+ # Here's the editor:
133
+ # <%= render partial: "user", layout: "editor", locals: { user: editor } %>
134
+ #
135
+ # <%# app/views/users/_user.html.erb %>
136
+ # Name: <%= user.name %>
137
+ #
138
+ # <%# app/views/users/_administrator.html.erb %>
139
+ # <div id="administrator">
140
+ # Budget: $<%= user.budget %>
141
+ # <%= yield %>
142
+ # </div>
143
+ #
144
+ # <%# app/views/users/_editor.html.erb %>
145
+ # <div id="editor">
146
+ # Deadline: <%= user.deadline %>
147
+ # <%= yield %>
148
+ # </div>
149
+ #
150
+ # ...this will return:
151
+ #
152
+ # Here's the administrator:
153
+ # <div id="administrator">
154
+ # Budget: $<%= user.budget %>
155
+ # Name: <%= user.name %>
156
+ # </div>
157
+ #
158
+ # Here's the editor:
159
+ # <div id="editor">
160
+ # Deadline: <%= user.deadline %>
161
+ # Name: <%= user.name %>
162
+ # </div>
163
+ #
164
+ # If a collection is given, the layout will be rendered once for each item in
165
+ # the collection. For example, these two snippets have the same output:
166
+ #
167
+ # <%# app/views/users/_user.html.erb %>
168
+ # Name: <%= user.name %>
169
+ #
170
+ # <%# app/views/users/index.html.erb %>
171
+ # <%# This does not use layouts %>
172
+ # <ul>
173
+ # <% users.each do |user| -%>
174
+ # <li>
175
+ # <%= render partial: "user", locals: { user: user } %>
176
+ # </li>
177
+ # <% end -%>
178
+ # </ul>
179
+ #
180
+ # <%# app/views/users/_li_layout.html.erb %>
181
+ # <li>
182
+ # <%= yield %>
183
+ # </li>
184
+ #
185
+ # <%# app/views/users/index.html.erb %>
186
+ # <ul>
187
+ # <%= render partial: "user", layout: "li_layout", collection: users %>
188
+ # </ul>
189
+ #
190
+ # Given two users whose names are Alice and Bob, these snippets return:
191
+ #
192
+ # <ul>
193
+ # <li>
194
+ # Name: Alice
195
+ # </li>
196
+ # <li>
197
+ # Name: Bob
198
+ # </li>
199
+ # </ul>
200
+ #
201
+ # The current object being rendered, as well as the object_counter, will be
202
+ # available as local variables inside the layout template under the same names
203
+ # as available in the partial.
204
+ #
205
+ # You can also apply a layout to a block within any template:
206
+ #
207
+ # <%# app/views/users/_chief.html.erb %>
208
+ # <%= render(layout: "administrator", locals: { user: chief }) do %>
209
+ # Title: <%= chief.title %>
210
+ # <% end %>
211
+ #
212
+ # ...this will return:
213
+ #
214
+ # <div id="administrator">
215
+ # Budget: $<%= user.budget %>
216
+ # Title: <%= chief.name %>
217
+ # </div>
218
+ #
219
+ # As you can see, the <tt>:locals</tt> hash is shared between both the partial and its layout.
220
+ class PartialRenderer < AbstractRenderer
221
+ include CollectionCaching
222
+
223
+ def initialize(lookup_context, options)
224
+ super(lookup_context)
225
+ @options = options
226
+ @locals = @options[:locals] || {}
227
+ @details = extract_details(@options)
228
+ end
229
+
230
+ def render(partial, context, block)
231
+ template = find_template(partial, template_keys(partial))
232
+
233
+ if !block && (layout = @options[:layout])
234
+ layout = find_template(layout.to_s, template_keys(partial))
235
+ end
236
+
237
+ render_partial_template(context, @locals, template, layout, block)
238
+ end
239
+
240
+ private
241
+ def template_keys(_)
242
+ @locals.keys
243
+ end
244
+
245
+ def render_partial_template(view, locals, template, layout, block)
246
+ ActiveSupport::Notifications.instrument(
247
+ "render_partial.action_view",
248
+ identifier: template.identifier,
249
+ layout: layout && layout.virtual_path,
250
+ locals: locals
251
+ ) do |payload|
252
+ content = template.render(view, locals, add_to_stack: !block) do |*name|
253
+ view._layout_for(*name, &block)
254
+ end
255
+
256
+ content = layout.render(view, locals) { content } if layout
257
+ payload[:cache_hit] = view.view_renderer.cache_hits[template.virtual_path]
258
+ build_rendered_template(content, template)
259
+ end
260
+ end
261
+
262
+ def find_template(path, locals)
263
+ prefixes = path.include?(?/) ? [] : @lookup_context.prefixes
264
+ @lookup_context.find_template(path, prefixes, true, locals, @details)
265
+ end
266
+ end
267
+ end