actionview 6.0.0

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.

Files changed (113) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +271 -0
  3. data/MIT-LICENSE +21 -0
  4. data/README.rdoc +40 -0
  5. data/lib/action_view.rb +98 -0
  6. data/lib/action_view/base.rb +312 -0
  7. data/lib/action_view/buffers.rb +67 -0
  8. data/lib/action_view/cache_expiry.rb +54 -0
  9. data/lib/action_view/context.rb +32 -0
  10. data/lib/action_view/dependency_tracker.rb +175 -0
  11. data/lib/action_view/digestor.rb +126 -0
  12. data/lib/action_view/flows.rb +76 -0
  13. data/lib/action_view/gem_version.rb +17 -0
  14. data/lib/action_view/helpers.rb +66 -0
  15. data/lib/action_view/helpers/active_model_helper.rb +55 -0
  16. data/lib/action_view/helpers/asset_tag_helper.rb +488 -0
  17. data/lib/action_view/helpers/asset_url_helper.rb +470 -0
  18. data/lib/action_view/helpers/atom_feed_helper.rb +205 -0
  19. data/lib/action_view/helpers/cache_helper.rb +271 -0
  20. data/lib/action_view/helpers/capture_helper.rb +216 -0
  21. data/lib/action_view/helpers/controller_helper.rb +36 -0
  22. data/lib/action_view/helpers/csp_helper.rb +26 -0
  23. data/lib/action_view/helpers/csrf_helper.rb +35 -0
  24. data/lib/action_view/helpers/date_helper.rb +1200 -0
  25. data/lib/action_view/helpers/debug_helper.rb +36 -0
  26. data/lib/action_view/helpers/form_helper.rb +2569 -0
  27. data/lib/action_view/helpers/form_options_helper.rb +896 -0
  28. data/lib/action_view/helpers/form_tag_helper.rb +920 -0
  29. data/lib/action_view/helpers/javascript_helper.rb +95 -0
  30. data/lib/action_view/helpers/number_helper.rb +456 -0
  31. data/lib/action_view/helpers/output_safety_helper.rb +70 -0
  32. data/lib/action_view/helpers/rendering_helper.rb +101 -0
  33. data/lib/action_view/helpers/sanitize_helper.rb +171 -0
  34. data/lib/action_view/helpers/tag_helper.rb +314 -0
  35. data/lib/action_view/helpers/tags.rb +44 -0
  36. data/lib/action_view/helpers/tags/base.rb +196 -0
  37. data/lib/action_view/helpers/tags/check_box.rb +66 -0
  38. data/lib/action_view/helpers/tags/checkable.rb +18 -0
  39. data/lib/action_view/helpers/tags/collection_check_boxes.rb +36 -0
  40. data/lib/action_view/helpers/tags/collection_helpers.rb +119 -0
  41. data/lib/action_view/helpers/tags/collection_radio_buttons.rb +31 -0
  42. data/lib/action_view/helpers/tags/collection_select.rb +30 -0
  43. data/lib/action_view/helpers/tags/color_field.rb +27 -0
  44. data/lib/action_view/helpers/tags/date_field.rb +15 -0
  45. data/lib/action_view/helpers/tags/date_select.rb +74 -0
  46. data/lib/action_view/helpers/tags/datetime_field.rb +32 -0
  47. data/lib/action_view/helpers/tags/datetime_local_field.rb +21 -0
  48. data/lib/action_view/helpers/tags/datetime_select.rb +10 -0
  49. data/lib/action_view/helpers/tags/email_field.rb +10 -0
  50. data/lib/action_view/helpers/tags/file_field.rb +10 -0
  51. data/lib/action_view/helpers/tags/grouped_collection_select.rb +31 -0
  52. data/lib/action_view/helpers/tags/hidden_field.rb +10 -0
  53. data/lib/action_view/helpers/tags/label.rb +81 -0
  54. data/lib/action_view/helpers/tags/month_field.rb +15 -0
  55. data/lib/action_view/helpers/tags/number_field.rb +20 -0
  56. data/lib/action_view/helpers/tags/password_field.rb +14 -0
  57. data/lib/action_view/helpers/tags/placeholderable.rb +24 -0
  58. data/lib/action_view/helpers/tags/radio_button.rb +33 -0
  59. data/lib/action_view/helpers/tags/range_field.rb +10 -0
  60. data/lib/action_view/helpers/tags/search_field.rb +27 -0
  61. data/lib/action_view/helpers/tags/select.rb +43 -0
  62. data/lib/action_view/helpers/tags/tel_field.rb +10 -0
  63. data/lib/action_view/helpers/tags/text_area.rb +24 -0
  64. data/lib/action_view/helpers/tags/text_field.rb +34 -0
  65. data/lib/action_view/helpers/tags/time_field.rb +15 -0
  66. data/lib/action_view/helpers/tags/time_select.rb +10 -0
  67. data/lib/action_view/helpers/tags/time_zone_select.rb +22 -0
  68. data/lib/action_view/helpers/tags/translator.rb +39 -0
  69. data/lib/action_view/helpers/tags/url_field.rb +10 -0
  70. data/lib/action_view/helpers/tags/week_field.rb +15 -0
  71. data/lib/action_view/helpers/text_helper.rb +486 -0
  72. data/lib/action_view/helpers/translation_helper.rb +145 -0
  73. data/lib/action_view/helpers/url_helper.rb +676 -0
  74. data/lib/action_view/layouts.rb +433 -0
  75. data/lib/action_view/locale/en.yml +56 -0
  76. data/lib/action_view/log_subscriber.rb +96 -0
  77. data/lib/action_view/lookup_context.rb +316 -0
  78. data/lib/action_view/model_naming.rb +14 -0
  79. data/lib/action_view/path_set.rb +95 -0
  80. data/lib/action_view/railtie.rb +105 -0
  81. data/lib/action_view/record_identifier.rb +112 -0
  82. data/lib/action_view/renderer/abstract_renderer.rb +108 -0
  83. data/lib/action_view/renderer/partial_renderer.rb +563 -0
  84. data/lib/action_view/renderer/partial_renderer/collection_caching.rb +103 -0
  85. data/lib/action_view/renderer/renderer.rb +68 -0
  86. data/lib/action_view/renderer/streaming_template_renderer.rb +105 -0
  87. data/lib/action_view/renderer/template_renderer.rb +108 -0
  88. data/lib/action_view/rendering.rb +171 -0
  89. data/lib/action_view/routing_url_for.rb +146 -0
  90. data/lib/action_view/tasks/cache_digests.rake +25 -0
  91. data/lib/action_view/template.rb +393 -0
  92. data/lib/action_view/template/error.rb +161 -0
  93. data/lib/action_view/template/handlers.rb +92 -0
  94. data/lib/action_view/template/handlers/builder.rb +25 -0
  95. data/lib/action_view/template/handlers/erb.rb +84 -0
  96. data/lib/action_view/template/handlers/erb/erubi.rb +87 -0
  97. data/lib/action_view/template/handlers/html.rb +11 -0
  98. data/lib/action_view/template/handlers/raw.rb +11 -0
  99. data/lib/action_view/template/html.rb +43 -0
  100. data/lib/action_view/template/inline.rb +22 -0
  101. data/lib/action_view/template/raw_file.rb +28 -0
  102. data/lib/action_view/template/resolver.rb +394 -0
  103. data/lib/action_view/template/sources.rb +13 -0
  104. data/lib/action_view/template/sources/file.rb +17 -0
  105. data/lib/action_view/template/text.rb +35 -0
  106. data/lib/action_view/template/types.rb +57 -0
  107. data/lib/action_view/test_case.rb +300 -0
  108. data/lib/action_view/testing/resolvers.rb +67 -0
  109. data/lib/action_view/unbound_template.rb +32 -0
  110. data/lib/action_view/version.rb +10 -0
  111. data/lib/action_view/view_paths.rb +129 -0
  112. data/lib/assets/compiled/rails-ujs.js +746 -0
  113. metadata +260 -0
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "action_view"
4
+ require "rails"
5
+
6
+ module ActionView
7
+ # = Action View Railtie
8
+ class Railtie < Rails::Engine # :nodoc:
9
+ NULL_OPTION = Object.new
10
+
11
+ config.action_view = ActiveSupport::OrderedOptions.new
12
+ config.action_view.embed_authenticity_token_in_remote_forms = nil
13
+ config.action_view.debug_missing_translation = true
14
+ config.action_view.default_enforce_utf8 = nil
15
+ config.action_view.finalize_compiled_template_methods = NULL_OPTION
16
+
17
+ config.eager_load_namespaces << ActionView
18
+
19
+ initializer "action_view.embed_authenticity_token_in_remote_forms" do |app|
20
+ ActiveSupport.on_load(:action_view) do
21
+ ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms =
22
+ app.config.action_view.delete(:embed_authenticity_token_in_remote_forms)
23
+ end
24
+ end
25
+
26
+ initializer "action_view.form_with_generates_remote_forms" do |app|
27
+ ActiveSupport.on_load(:action_view) do
28
+ form_with_generates_remote_forms = app.config.action_view.delete(:form_with_generates_remote_forms)
29
+ ActionView::Helpers::FormHelper.form_with_generates_remote_forms = form_with_generates_remote_forms
30
+ end
31
+ end
32
+
33
+ initializer "action_view.form_with_generates_ids" do |app|
34
+ ActiveSupport.on_load(:action_view) do
35
+ form_with_generates_ids = app.config.action_view.delete(:form_with_generates_ids)
36
+ unless form_with_generates_ids.nil?
37
+ ActionView::Helpers::FormHelper.form_with_generates_ids = form_with_generates_ids
38
+ end
39
+ end
40
+ end
41
+
42
+ initializer "action_view.default_enforce_utf8" do |app|
43
+ ActiveSupport.on_load(:action_view) do
44
+ default_enforce_utf8 = app.config.action_view.delete(:default_enforce_utf8)
45
+ unless default_enforce_utf8.nil?
46
+ ActionView::Helpers::FormTagHelper.default_enforce_utf8 = default_enforce_utf8
47
+ end
48
+ end
49
+ end
50
+
51
+ initializer "action_view.finalize_compiled_template_methods" do |app|
52
+ ActiveSupport.on_load(:action_view) do
53
+ option = app.config.action_view.delete(:finalize_compiled_template_methods)
54
+
55
+ if option != NULL_OPTION
56
+ ActiveSupport::Deprecation.warn "action_view.finalize_compiled_template_methods is deprecated and has no effect"
57
+ end
58
+ end
59
+ end
60
+
61
+ initializer "action_view.logger" do
62
+ ActiveSupport.on_load(:action_view) { self.logger ||= Rails.logger }
63
+ end
64
+
65
+ initializer "action_view.set_configs" do |app|
66
+ ActiveSupport.on_load(:action_view) do
67
+ app.config.action_view.each do |k, v|
68
+ send "#{k}=", v
69
+ end
70
+ end
71
+ end
72
+
73
+ initializer "action_view.caching" do |app|
74
+ ActiveSupport.on_load(:action_view) do
75
+ if app.config.action_view.cache_template_loading.nil?
76
+ ActionView::Resolver.caching = app.config.cache_classes
77
+ end
78
+ end
79
+ end
80
+
81
+ initializer "action_view.per_request_digest_cache" do |app|
82
+ ActiveSupport.on_load(:action_view) do
83
+ unless ActionView::Resolver.caching?
84
+ app.executor.to_run ActionView::CacheExpiry::Executor.new(watcher: app.config.file_watcher)
85
+ end
86
+ end
87
+ end
88
+
89
+ initializer "action_view.setup_action_pack" do |app|
90
+ ActiveSupport.on_load(:action_controller) do
91
+ ActionView::RoutingUrlFor.include(ActionDispatch::Routing::UrlFor)
92
+ end
93
+ end
94
+
95
+ initializer "action_view.collection_caching", after: "action_controller.set_configs" do |app|
96
+ PartialRenderer.collection_cache = app.config.action_controller.cache_store
97
+ end
98
+
99
+ rake_tasks do |app|
100
+ unless app.config.api_only
101
+ load "action_view/tasks/cache_digests.rake"
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/module"
4
+ require "action_view/model_naming"
5
+
6
+ module ActionView
7
+ # RecordIdentifier encapsulates methods used by various ActionView helpers
8
+ # to associate records with DOM elements.
9
+ #
10
+ # Consider for example the following code that form of post:
11
+ #
12
+ # <%= form_for(post) do |f| %>
13
+ # <%= f.text_field :body %>
14
+ # <% end %>
15
+ #
16
+ # When +post+ is a new, unsaved ActiveRecord::Base instance, the resulting HTML
17
+ # is:
18
+ #
19
+ # <form class="new_post" id="new_post" action="/posts" accept-charset="UTF-8" method="post">
20
+ # <input type="text" name="post[body]" id="post_body" />
21
+ # </form>
22
+ #
23
+ # When +post+ is a persisted ActiveRecord::Base instance, the resulting HTML
24
+ # is:
25
+ #
26
+ # <form class="edit_post" id="edit_post_42" action="/posts/42" accept-charset="UTF-8" method="post">
27
+ # <input type="text" value="What a wonderful world!" name="post[body]" id="post_body" />
28
+ # </form>
29
+ #
30
+ # In both cases, the +id+ and +class+ of the wrapping DOM element are
31
+ # automatically generated, following naming conventions encapsulated by the
32
+ # RecordIdentifier methods #dom_id and #dom_class:
33
+ #
34
+ # dom_id(Post.new) # => "new_post"
35
+ # dom_class(Post.new) # => "post"
36
+ # dom_id(Post.find 42) # => "post_42"
37
+ # dom_class(Post.find 42) # => "post"
38
+ #
39
+ # Note that these methods do not strictly require +Post+ to be a subclass of
40
+ # ActiveRecord::Base.
41
+ # Any +Post+ class will work as long as its instances respond to +to_key+
42
+ # and +model_name+, given that +model_name+ responds to +param_key+.
43
+ # For instance:
44
+ #
45
+ # class Post
46
+ # attr_accessor :to_key
47
+ #
48
+ # def model_name
49
+ # OpenStruct.new param_key: 'post'
50
+ # end
51
+ #
52
+ # def self.find(id)
53
+ # new.tap { |post| post.to_key = [id] }
54
+ # end
55
+ # end
56
+ module RecordIdentifier
57
+ extend self
58
+ extend ModelNaming
59
+
60
+ include ModelNaming
61
+
62
+ JOIN = "_"
63
+ NEW = "new"
64
+
65
+ # The DOM class convention is to use the singular form of an object or class.
66
+ #
67
+ # dom_class(post) # => "post"
68
+ # dom_class(Person) # => "person"
69
+ #
70
+ # If you need to address multiple instances of the same class in the same view, you can prefix the dom_class:
71
+ #
72
+ # dom_class(post, :edit) # => "edit_post"
73
+ # dom_class(Person, :edit) # => "edit_person"
74
+ def dom_class(record_or_class, prefix = nil)
75
+ singular = model_name_from_record_or_class(record_or_class).param_key
76
+ prefix ? "#{prefix}#{JOIN}#{singular}" : singular
77
+ end
78
+
79
+ # The DOM id convention is to use the singular form of an object or class with the id following an underscore.
80
+ # If no id is found, prefix with "new_" instead.
81
+ #
82
+ # dom_id(Post.find(45)) # => "post_45"
83
+ # dom_id(Post.new) # => "new_post"
84
+ #
85
+ # If you need to address multiple instances of the same class in the same view, you can prefix the dom_id:
86
+ #
87
+ # dom_id(Post.find(45), :edit) # => "edit_post_45"
88
+ # dom_id(Post.new, :custom) # => "custom_post"
89
+ def dom_id(record, prefix = nil)
90
+ if record_id = record_key_for_dom_id(record)
91
+ "#{dom_class(record, prefix)}#{JOIN}#{record_id}"
92
+ else
93
+ dom_class(record, prefix || NEW)
94
+ end
95
+ end
96
+
97
+ private
98
+
99
+ # Returns a string representation of the key attribute(s) that is suitable for use in an HTML DOM id.
100
+ # This can be overwritten to customize the default generated string representation if desired.
101
+ # If you need to read back a key from a dom_id in order to query for the underlying database record,
102
+ # you should write a helper like 'person_record_from_dom_id' that will extract the key either based
103
+ # on the default implementation (which just joins all key attributes with '_') or on your own
104
+ # overwritten version of the method. By default, this implementation passes the key string through a
105
+ # method that replaces all characters that are invalid inside DOM ids, with valid ones. You need to
106
+ # make sure yourself that your dom ids are valid, in case you overwrite this method.
107
+ def record_key_for_dom_id(record) # :doc:
108
+ key = convert_to_model(record).to_key
109
+ key ? key.join(JOIN) : key
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionView
4
+ # This class defines the interface for a renderer. Each class that
5
+ # subclasses +AbstractRenderer+ is used by the base +Renderer+ class to
6
+ # render a specific type of object.
7
+ #
8
+ # The base +Renderer+ class uses its +render+ method to delegate to the
9
+ # renderers. These currently consist of
10
+ #
11
+ # PartialRenderer - Used for rendering partials
12
+ # TemplateRenderer - Used for rendering other types of templates
13
+ # StreamingTemplateRenderer - Used for streaming
14
+ #
15
+ # Whenever the +render+ method is called on the base +Renderer+ class, a new
16
+ # renderer object of the correct type is created, and the +render+ method on
17
+ # that new object is called in turn. This abstracts the setup and rendering
18
+ # into a separate classes for partials and templates.
19
+ class AbstractRenderer #:nodoc:
20
+ delegate :template_exists?, :any_templates?, :formats, to: :@lookup_context
21
+
22
+ def initialize(lookup_context)
23
+ @lookup_context = lookup_context
24
+ end
25
+
26
+ def render
27
+ raise NotImplementedError
28
+ end
29
+
30
+ class RenderedCollection # :nodoc:
31
+ def self.empty(format)
32
+ EmptyCollection.new format
33
+ end
34
+
35
+ attr_reader :rendered_templates
36
+
37
+ def initialize(rendered_templates, spacer)
38
+ @rendered_templates = rendered_templates
39
+ @spacer = spacer
40
+ end
41
+
42
+ def body
43
+ @rendered_templates.map(&:body).join(@spacer.body).html_safe
44
+ end
45
+
46
+ def format
47
+ rendered_templates.first.format
48
+ end
49
+
50
+ class EmptyCollection
51
+ attr_reader :format
52
+
53
+ def initialize(format)
54
+ @format = format
55
+ end
56
+
57
+ def body; nil; end
58
+ end
59
+ end
60
+
61
+ class RenderedTemplate # :nodoc:
62
+ attr_reader :body, :layout, :template
63
+
64
+ def initialize(body, layout, template)
65
+ @body = body
66
+ @layout = layout
67
+ @template = template
68
+ end
69
+
70
+ def format
71
+ template.format
72
+ end
73
+
74
+ EMPTY_SPACER = Struct.new(:body).new
75
+ end
76
+
77
+ private
78
+
79
+ def extract_details(options) # :doc:
80
+ @lookup_context.registered_details.each_with_object({}) do |key, details|
81
+ value = options[key]
82
+
83
+ details[key] = Array(value) if value
84
+ end
85
+ end
86
+
87
+ def instrument(name, **options) # :doc:
88
+ ActiveSupport::Notifications.instrument("render_#{name}.action_view", options) do |payload|
89
+ yield payload
90
+ end
91
+ end
92
+
93
+ def prepend_formats(formats) # :doc:
94
+ formats = Array(formats)
95
+ return if formats.empty? || @lookup_context.html_fallback_for_js
96
+
97
+ @lookup_context.formats = formats | @lookup_context.formats
98
+ end
99
+
100
+ def build_rendered_template(content, template, layout = nil)
101
+ RenderedTemplate.new content, layout, template
102
+ end
103
+
104
+ def build_rendered_collection(templates, spacer)
105
+ RenderedCollection.new templates, spacer
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,563 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "concurrent/map"
4
+ require "action_view/renderer/partial_renderer/collection_caching"
5
+
6
+ module ActionView
7
+ class PartialIteration
8
+ # The number of iterations that will be done by the partial.
9
+ attr_reader :size
10
+
11
+ # The current iteration of the partial.
12
+ attr_reader :index
13
+
14
+ def initialize(size)
15
+ @size = size
16
+ @index = 0
17
+ end
18
+
19
+ # Check if this is the first iteration of the partial.
20
+ def first?
21
+ index == 0
22
+ end
23
+
24
+ # Check if this is the last iteration of the partial.
25
+ def last?
26
+ index == size - 1
27
+ end
28
+
29
+ def iterate! # :nodoc:
30
+ @index += 1
31
+ end
32
+ end
33
+
34
+ # = Action View Partials
35
+ #
36
+ # There's also a convenience method for rendering sub templates within the current controller that depends on a
37
+ # single object (we call this kind of sub templates for partials). It relies on the fact that partials should
38
+ # follow the naming convention of being prefixed with an underscore -- as to separate them from regular
39
+ # templates that could be rendered on their own.
40
+ #
41
+ # In a template for Advertiser#account:
42
+ #
43
+ # <%= render partial: "account" %>
44
+ #
45
+ # This would render "advertiser/_account.html.erb".
46
+ #
47
+ # In another template for Advertiser#buy, we could have:
48
+ #
49
+ # <%= render partial: "account", locals: { account: @buyer } %>
50
+ #
51
+ # <% @advertisements.each do |ad| %>
52
+ # <%= render partial: "ad", locals: { ad: ad } %>
53
+ # <% end %>
54
+ #
55
+ # This would first render <tt>advertiser/_account.html.erb</tt> with <tt>@buyer</tt> passed in as the local variable +account+, then
56
+ # render <tt>advertiser/_ad.html.erb</tt> and pass the local variable +ad+ to the template for display.
57
+ #
58
+ # == The :as and :object options
59
+ #
60
+ # By default ActionView::PartialRenderer doesn't have any local variables.
61
+ # The <tt>:object</tt> option can be used to pass an object to the partial. For instance:
62
+ #
63
+ # <%= render partial: "account", object: @buyer %>
64
+ #
65
+ # would provide the <tt>@buyer</tt> object to the partial, available under the local variable +account+ and is
66
+ # equivalent to:
67
+ #
68
+ # <%= render partial: "account", locals: { account: @buyer } %>
69
+ #
70
+ # With the <tt>:as</tt> option we can specify a different name for said local variable. For example, if we
71
+ # wanted it to be +user+ instead of +account+ we'd do:
72
+ #
73
+ # <%= render partial: "account", object: @buyer, as: 'user' %>
74
+ #
75
+ # This is equivalent to
76
+ #
77
+ # <%= render partial: "account", locals: { user: @buyer } %>
78
+ #
79
+ # == \Rendering a collection of partials
80
+ #
81
+ # The example of partial use describes a familiar pattern where a template needs to iterate over an array and
82
+ # render a sub template for each of the elements. This pattern has been implemented as a single method that
83
+ # accepts an array and renders a partial by the same name as the elements contained within. So the three-lined
84
+ # example in "Using partials" can be rewritten with a single line:
85
+ #
86
+ # <%= render partial: "ad", collection: @advertisements %>
87
+ #
88
+ # This will render <tt>advertiser/_ad.html.erb</tt> and pass the local variable +ad+ to the template for display. An
89
+ # iteration object will automatically be made available to the template with a name of the form
90
+ # +partial_name_iteration+. The iteration object has knowledge about which index the current object has in
91
+ # the collection and the total size of the collection. The iteration object also has two convenience methods,
92
+ # +first?+ and +last?+. In the case of the example above, the template would be fed +ad_iteration+.
93
+ # For backwards compatibility the +partial_name_counter+ is still present and is mapped to the iteration's
94
+ # +index+ method.
95
+ #
96
+ # The <tt>:as</tt> option may be used when rendering partials.
97
+ #
98
+ # You can specify a partial to be rendered between elements via the <tt>:spacer_template</tt> option.
99
+ # The following example will render <tt>advertiser/_ad_divider.html.erb</tt> between each ad partial:
100
+ #
101
+ # <%= render partial: "ad", collection: @advertisements, spacer_template: "ad_divider" %>
102
+ #
103
+ # If the given <tt>:collection</tt> is +nil+ or empty, <tt>render</tt> will return +nil+. This will allow you
104
+ # to specify a text which will be displayed instead by using this form:
105
+ #
106
+ # <%= render(partial: "ad", collection: @advertisements) || "There's no ad to be displayed" %>
107
+ #
108
+ # == \Rendering shared partials
109
+ #
110
+ # Two controllers can share a set of partials and render them like this:
111
+ #
112
+ # <%= render partial: "advertisement/ad", locals: { ad: @advertisement } %>
113
+ #
114
+ # This will render the partial <tt>advertisement/_ad.html.erb</tt> regardless of which controller this is being called from.
115
+ #
116
+ # == \Rendering objects that respond to +to_partial_path+
117
+ #
118
+ # Instead of explicitly naming the location of a partial, you can also let PartialRenderer do the work
119
+ # and pick the proper path by checking +to_partial_path+ method.
120
+ #
121
+ # # @account.to_partial_path returns 'accounts/account', so it can be used to replace:
122
+ # # <%= render partial: "accounts/account", locals: { account: @account} %>
123
+ # <%= render partial: @account %>
124
+ #
125
+ # # @posts is an array of Post instances, so every post record returns 'posts/post' on +to_partial_path+,
126
+ # # that's why we can replace:
127
+ # # <%= render partial: "posts/post", collection: @posts %>
128
+ # <%= render partial: @posts %>
129
+ #
130
+ # == \Rendering the default case
131
+ #
132
+ # If you're not going to be using any of the options like collections or layouts, you can also use the short-hand
133
+ # defaults of render to render partials. Examples:
134
+ #
135
+ # # Instead of <%= render partial: "account" %>
136
+ # <%= render "account" %>
137
+ #
138
+ # # Instead of <%= render partial: "account", locals: { account: @buyer } %>
139
+ # <%= render "account", account: @buyer %>
140
+ #
141
+ # # @account.to_partial_path returns 'accounts/account', so it can be used to replace:
142
+ # # <%= render partial: "accounts/account", locals: { account: @account} %>
143
+ # <%= render @account %>
144
+ #
145
+ # # @posts is an array of Post instances, so every post record returns 'posts/post' on +to_partial_path+,
146
+ # # that's why we can replace:
147
+ # # <%= render partial: "posts/post", collection: @posts %>
148
+ # <%= render @posts %>
149
+ #
150
+ # == \Rendering partials with layouts
151
+ #
152
+ # Partials can have their own layouts applied to them. These layouts are different than the ones that are
153
+ # specified globally for the entire action, but they work in a similar fashion. Imagine a list with two types
154
+ # of users:
155
+ #
156
+ # <%# app/views/users/index.html.erb %>
157
+ # Here's the administrator:
158
+ # <%= render partial: "user", layout: "administrator", locals: { user: administrator } %>
159
+ #
160
+ # Here's the editor:
161
+ # <%= render partial: "user", layout: "editor", locals: { user: editor } %>
162
+ #
163
+ # <%# app/views/users/_user.html.erb %>
164
+ # Name: <%= user.name %>
165
+ #
166
+ # <%# app/views/users/_administrator.html.erb %>
167
+ # <div id="administrator">
168
+ # Budget: $<%= user.budget %>
169
+ # <%= yield %>
170
+ # </div>
171
+ #
172
+ # <%# app/views/users/_editor.html.erb %>
173
+ # <div id="editor">
174
+ # Deadline: <%= user.deadline %>
175
+ # <%= yield %>
176
+ # </div>
177
+ #
178
+ # ...this will return:
179
+ #
180
+ # Here's the administrator:
181
+ # <div id="administrator">
182
+ # Budget: $<%= user.budget %>
183
+ # Name: <%= user.name %>
184
+ # </div>
185
+ #
186
+ # Here's the editor:
187
+ # <div id="editor">
188
+ # Deadline: <%= user.deadline %>
189
+ # Name: <%= user.name %>
190
+ # </div>
191
+ #
192
+ # If a collection is given, the layout will be rendered once for each item in
193
+ # the collection. For example, these two snippets have the same output:
194
+ #
195
+ # <%# app/views/users/_user.html.erb %>
196
+ # Name: <%= user.name %>
197
+ #
198
+ # <%# app/views/users/index.html.erb %>
199
+ # <%# This does not use layouts %>
200
+ # <ul>
201
+ # <% users.each do |user| -%>
202
+ # <li>
203
+ # <%= render partial: "user", locals: { user: user } %>
204
+ # </li>
205
+ # <% end -%>
206
+ # </ul>
207
+ #
208
+ # <%# app/views/users/_li_layout.html.erb %>
209
+ # <li>
210
+ # <%= yield %>
211
+ # </li>
212
+ #
213
+ # <%# app/views/users/index.html.erb %>
214
+ # <ul>
215
+ # <%= render partial: "user", layout: "li_layout", collection: users %>
216
+ # </ul>
217
+ #
218
+ # Given two users whose names are Alice and Bob, these snippets return:
219
+ #
220
+ # <ul>
221
+ # <li>
222
+ # Name: Alice
223
+ # </li>
224
+ # <li>
225
+ # Name: Bob
226
+ # </li>
227
+ # </ul>
228
+ #
229
+ # The current object being rendered, as well as the object_counter, will be
230
+ # available as local variables inside the layout template under the same names
231
+ # as available in the partial.
232
+ #
233
+ # You can also apply a layout to a block within any template:
234
+ #
235
+ # <%# app/views/users/_chief.html.erb %>
236
+ # <%= render(layout: "administrator", locals: { user: chief }) do %>
237
+ # Title: <%= chief.title %>
238
+ # <% end %>
239
+ #
240
+ # ...this will return:
241
+ #
242
+ # <div id="administrator">
243
+ # Budget: $<%= user.budget %>
244
+ # Title: <%= chief.name %>
245
+ # </div>
246
+ #
247
+ # As you can see, the <tt>:locals</tt> hash is shared between both the partial and its layout.
248
+ #
249
+ # If you pass arguments to "yield" then this will be passed to the block. One way to use this is to pass
250
+ # an array to layout and treat it as an enumerable.
251
+ #
252
+ # <%# app/views/users/_user.html.erb %>
253
+ # <div class="user">
254
+ # Budget: $<%= user.budget %>
255
+ # <%= yield user %>
256
+ # </div>
257
+ #
258
+ # <%# app/views/users/index.html.erb %>
259
+ # <%= render layout: @users do |user| %>
260
+ # Title: <%= user.title %>
261
+ # <% end %>
262
+ #
263
+ # This will render the layout for each user and yield to the block, passing the user, each time.
264
+ #
265
+ # You can also yield multiple times in one layout and use block arguments to differentiate the sections.
266
+ #
267
+ # <%# app/views/users/_user.html.erb %>
268
+ # <div class="user">
269
+ # <%= yield user, :header %>
270
+ # Budget: $<%= user.budget %>
271
+ # <%= yield user, :footer %>
272
+ # </div>
273
+ #
274
+ # <%# app/views/users/index.html.erb %>
275
+ # <%= render layout: @users do |user, section| %>
276
+ # <%- case section when :header -%>
277
+ # Title: <%= user.title %>
278
+ # <%- when :footer -%>
279
+ # Deadline: <%= user.deadline %>
280
+ # <%- end -%>
281
+ # <% end %>
282
+ class PartialRenderer < AbstractRenderer
283
+ include CollectionCaching
284
+
285
+ PREFIXED_PARTIAL_NAMES = Concurrent::Map.new do |h, k|
286
+ h[k] = Concurrent::Map.new
287
+ end
288
+
289
+ def initialize(*)
290
+ super
291
+ @context_prefix = @lookup_context.prefixes.first
292
+ end
293
+
294
+ def render(context, options, block)
295
+ as = as_variable(options)
296
+ setup(context, options, as, block)
297
+
298
+ if @path
299
+ if @has_object || @collection
300
+ @variable, @variable_counter, @variable_iteration = retrieve_variable(@path, as)
301
+ @template_keys = retrieve_template_keys(@variable)
302
+ else
303
+ @template_keys = @locals.keys
304
+ end
305
+ template = find_partial(@path, @template_keys)
306
+ @variable ||= template.variable
307
+ else
308
+ if options[:cached]
309
+ raise NotImplementedError, "render caching requires a template. Please specify a partial when rendering"
310
+ end
311
+ template = nil
312
+ end
313
+
314
+ if @collection
315
+ render_collection(context, template)
316
+ else
317
+ render_partial(context, template)
318
+ end
319
+ end
320
+
321
+ private
322
+
323
+ def render_collection(view, template)
324
+ identifier = (template && template.identifier) || @path
325
+ instrument(:collection, identifier: identifier, count: @collection.size) do |payload|
326
+ return RenderedCollection.empty(@lookup_context.formats.first) if @collection.blank?
327
+
328
+ spacer = if @options.key?(:spacer_template)
329
+ spacer_template = find_template(@options[:spacer_template], @locals.keys)
330
+ build_rendered_template(spacer_template.render(view, @locals), spacer_template)
331
+ else
332
+ RenderedTemplate::EMPTY_SPACER
333
+ end
334
+
335
+ collection_body = if template
336
+ cache_collection_render(payload, view, template) do
337
+ collection_with_template(view, template)
338
+ end
339
+ else
340
+ collection_without_template(view)
341
+ end
342
+ build_rendered_collection(collection_body, spacer)
343
+ end
344
+ end
345
+
346
+ def render_partial(view, template)
347
+ instrument(:partial, identifier: template.identifier) do |payload|
348
+ locals, block = @locals, @block
349
+ object, as = @object, @variable
350
+
351
+ if !block && (layout = @options[:layout])
352
+ layout = find_template(layout.to_s, @template_keys)
353
+ end
354
+
355
+ object = locals[as] if object.nil? # Respect object when object is false
356
+ locals[as] = object if @has_object
357
+
358
+ content = template.render(view, locals) do |*name|
359
+ view._layout_for(*name, &block)
360
+ end
361
+
362
+ content = layout.render(view, locals) { content } if layout
363
+ payload[:cache_hit] = view.view_renderer.cache_hits[template.virtual_path]
364
+ build_rendered_template(content, template, layout)
365
+ end
366
+ end
367
+
368
+ # Sets up instance variables needed for rendering a partial. This method
369
+ # finds the options and details and extracts them. The method also contains
370
+ # logic that handles the type of object passed in as the partial.
371
+ #
372
+ # If +options[:partial]+ is a string, then the <tt>@path</tt> instance variable is
373
+ # set to that string. Otherwise, the +options[:partial]+ object must
374
+ # respond to +to_partial_path+ in order to setup the path.
375
+ def setup(context, options, as, block)
376
+ @options = options
377
+ @block = block
378
+
379
+ @locals = options[:locals] || {}
380
+ @details = extract_details(options)
381
+
382
+ partial = options[:partial]
383
+
384
+ if String === partial
385
+ @has_object = options.key?(:object)
386
+ @object = options[:object]
387
+ @collection = collection_from_options
388
+ @path = partial
389
+ else
390
+ @has_object = true
391
+ @object = partial
392
+ @collection = collection_from_object || collection_from_options
393
+
394
+ if @collection
395
+ paths = @collection_data = @collection.map { |o| partial_path(o, context) }
396
+ if paths.uniq.length == 1
397
+ @path = paths.first
398
+ else
399
+ paths.map! { |path| retrieve_variable(path, as).unshift(path) }
400
+ @path = nil
401
+ end
402
+ else
403
+ @path = partial_path(@object, context)
404
+ end
405
+ end
406
+
407
+ self
408
+ end
409
+
410
+ def as_variable(options)
411
+ if as = options[:as]
412
+ raise_invalid_option_as(as) unless /\A[a-z_]\w*\z/.match?(as.to_s)
413
+ as.to_sym
414
+ end
415
+ end
416
+
417
+ def collection_from_options
418
+ if @options.key?(:collection)
419
+ collection = @options[:collection]
420
+ collection ? collection.to_a : []
421
+ end
422
+ end
423
+
424
+ def collection_from_object
425
+ @object.to_ary if @object.respond_to?(:to_ary)
426
+ end
427
+
428
+ def find_partial(path, template_keys)
429
+ find_template(path, template_keys)
430
+ end
431
+
432
+ def find_template(path, locals)
433
+ prefixes = path.include?(?/) ? [] : @lookup_context.prefixes
434
+ @lookup_context.find_template(path, prefixes, true, locals, @details)
435
+ end
436
+
437
+ def collection_with_template(view, template)
438
+ locals = @locals
439
+ as, counter, iteration = @variable, @variable_counter, @variable_iteration
440
+
441
+ if layout = @options[:layout]
442
+ layout = find_template(layout, @template_keys)
443
+ end
444
+
445
+ partial_iteration = PartialIteration.new(@collection.size)
446
+ locals[iteration] = partial_iteration
447
+
448
+ @collection.map do |object|
449
+ locals[as] = object
450
+ locals[counter] = partial_iteration.index
451
+
452
+ content = template.render(view, locals)
453
+ content = layout.render(view, locals) { content } if layout
454
+ partial_iteration.iterate!
455
+ build_rendered_template(content, template, layout)
456
+ end
457
+ end
458
+
459
+ def collection_without_template(view)
460
+ locals, collection_data = @locals, @collection_data
461
+ cache = {}
462
+ keys = @locals.keys
463
+
464
+ partial_iteration = PartialIteration.new(@collection.size)
465
+
466
+ @collection.map do |object|
467
+ index = partial_iteration.index
468
+ path, as, counter, iteration = collection_data[index]
469
+
470
+ locals[as] = object
471
+ locals[counter] = index
472
+ locals[iteration] = partial_iteration
473
+
474
+ template = (cache[path] ||= find_template(path, keys + [as, counter, iteration]))
475
+ content = template.render(view, locals)
476
+ partial_iteration.iterate!
477
+ build_rendered_template(content, template)
478
+ end
479
+ end
480
+
481
+ # Obtains the path to where the object's partial is located. If the object
482
+ # responds to +to_partial_path+, then +to_partial_path+ will be called and
483
+ # will provide the path. If the object does not respond to +to_partial_path+,
484
+ # then an +ArgumentError+ is raised.
485
+ #
486
+ # If +prefix_partial_path_with_controller_namespace+ is true, then this
487
+ # method will prefix the partial paths with a namespace.
488
+ def partial_path(object, view)
489
+ object = object.to_model if object.respond_to?(:to_model)
490
+
491
+ path = if object.respond_to?(:to_partial_path)
492
+ object.to_partial_path
493
+ else
494
+ raise ArgumentError.new("'#{object.inspect}' is not an ActiveModel-compatible object. It must implement :to_partial_path.")
495
+ end
496
+
497
+ if view.prefix_partial_path_with_controller_namespace
498
+ prefixed_partial_names[path] ||= merge_prefix_into_object_path(@context_prefix, path.dup)
499
+ else
500
+ path
501
+ end
502
+ end
503
+
504
+ def prefixed_partial_names
505
+ @prefixed_partial_names ||= PREFIXED_PARTIAL_NAMES[@context_prefix]
506
+ end
507
+
508
+ def merge_prefix_into_object_path(prefix, object_path)
509
+ if prefix.include?(?/) && object_path.include?(?/)
510
+ prefixes = []
511
+ prefix_array = File.dirname(prefix).split("/")
512
+ object_path_array = object_path.split("/")[0..-3] # skip model dir & partial
513
+
514
+ prefix_array.each_with_index do |dir, index|
515
+ break if dir == object_path_array[index]
516
+ prefixes << dir
517
+ end
518
+
519
+ (prefixes << object_path).join("/")
520
+ else
521
+ object_path
522
+ end
523
+ end
524
+
525
+ def retrieve_template_keys(variable)
526
+ keys = @locals.keys
527
+ keys << variable
528
+ if @collection
529
+ keys << @variable_counter
530
+ keys << @variable_iteration
531
+ end
532
+ keys
533
+ end
534
+
535
+ def retrieve_variable(path, as)
536
+ variable = as || begin
537
+ base = path[-1] == "/" ? "" : File.basename(path)
538
+ raise_invalid_identifier(path) unless base =~ /\A_?(.*?)(?:\.\w+)*\z/
539
+ $1.to_sym
540
+ end
541
+ if @collection
542
+ variable_counter = :"#{variable}_counter"
543
+ variable_iteration = :"#{variable}_iteration"
544
+ end
545
+ [variable, variable_counter, variable_iteration]
546
+ end
547
+
548
+ IDENTIFIER_ERROR_MESSAGE = "The partial name (%s) is not a valid Ruby identifier; " \
549
+ "make sure your partial name starts with underscore."
550
+
551
+ OPTION_AS_ERROR_MESSAGE = "The value (%s) of the option `as` is not a valid Ruby identifier; " \
552
+ "make sure it starts with lowercase letter, " \
553
+ "and is followed by any combination of letters, numbers and underscores."
554
+
555
+ def raise_invalid_identifier(path)
556
+ raise ArgumentError.new(IDENTIFIER_ERROR_MESSAGE % (path))
557
+ end
558
+
559
+ def raise_invalid_option_as(as)
560
+ raise ArgumentError.new(OPTION_AS_ERROR_MESSAGE % (as))
561
+ end
562
+ end
563
+ end