omg-actionview 8.0.0.alpha1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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