diff options
Diffstat (limited to 'actionpack/lib/action_view/renderer/partial_renderer.rb')
-rw-r--r-- | actionpack/lib/action_view/renderer/partial_renderer.rb | 202 |
1 files changed, 117 insertions, 85 deletions
diff --git a/actionpack/lib/action_view/renderer/partial_renderer.rb b/actionpack/lib/action_view/renderer/partial_renderer.rb index 245a19deec..37f93a13fc 100644 --- a/actionpack/lib/action_view/renderer/partial_renderer.rb +++ b/actionpack/lib/action_view/renderer/partial_renderer.rb @@ -1,4 +1,4 @@ -require 'active_support/core_ext/object/blank' +require 'thread_safe' module ActionView # = Action View Partials @@ -10,16 +10,16 @@ module ActionView # # In a template for Advertiser#account: # - # <%= render :partial => "account" %> + # <%= render partial: "account" %> # # This would render "advertiser/_account.html.erb". # # In another template for Advertiser#buy, we could have: # - # <%= render :partial => "account", :locals => { :account => @buyer } %> + # <%= render partial: "account", locals: { account: @buyer } %> # # <% @advertisements.each do |ad| %> - # <%= render :partial => "ad", :locals => { :ad => ad } %> + # <%= render partial: "ad", locals: { ad: ad } %> # <% end %> # # This would first render "advertiser/_account.html.erb" with @buyer passed in as the local variable +account+, then @@ -30,21 +30,21 @@ module ActionView # By default <tt>ActionView::PartialRenderer</tt> doesn't have any local variables. # The <tt>:object</tt> option can be used to pass an object to the partial. For instance: # - # <%= render :partial => "account", :object => @buyer %> + # <%= render partial: "account", object: @buyer %> # - # would provide the +@buyer+ object to the partial, available under the local variable +account+ and is + # would provide the <tt>@buyer</tt> object to the partial, available under the local variable +account+ and is # equivalent to: # - # <%= render :partial => "account", :locals => { :account => @buyer } %> + # <%= render partial: "account", locals: { account: @buyer } %> # # With the <tt>:as</tt> option we can specify a different name for said local variable. For example, if we # wanted it to be +user+ instead of +account+ we'd do: # - # <%= render :partial => "account", :object => @buyer, :as => 'user' %> + # <%= render partial: "account", object: @buyer, as: 'user' %> # # This is equivalent to # - # <%= render :partial => "account", :locals => { :user => @buyer } %> + # <%= render partial: "account", locals: { user: @buyer } %> # # == Rendering a collection of partials # @@ -53,7 +53,7 @@ module ActionView # accepts an array and renders a partial by the same name as the elements contained within. So the three-lined # example in "Using partials" can be rewritten with a single line: # - # <%= render :partial => "ad", :collection => @advertisements %> + # <%= render partial: "ad", collection: @advertisements %> # # This will render "advertiser/_ad.html.erb" and pass the local variable +ad+ to the template for display. An # iteration counter will automatically be made available to the template with a name of the form @@ -64,12 +64,12 @@ module ActionView # You can specify a partial to be rendered between elements via the <tt>:spacer_template</tt> option. # The following example will render <tt>advertiser/_ad_divider.html.erb</tt> between each ad partial: # - # <%= render :partial => "ad", :collection => @advertisements, :spacer_template => "ad_divider" %> + # <%= render partial: "ad", collection: @advertisements, spacer_template: "ad_divider" %> # # If the given <tt>:collection</tt> is nil or empty, <tt>render</tt> will return nil. This will allow you # to specify a text which will displayed instead by using this form: # - # <%= render(:partial => "ad", :collection => @advertisements) || "There's no ad to be displayed" %> + # <%= render(partial: "ad", collection: @advertisements) || "There's no ad to be displayed" %> # # NOTE: Due to backwards compatibility concerns, the collection can't be one of hashes. Normally you'd also # just keep domain objects, like Active Records, in there. @@ -78,7 +78,7 @@ module ActionView # # Two controllers can share a set of partials and render them like this: # - # <%= render :partial => "advertisement/ad", :locals => { :ad => @advertisement } %> + # <%= render partial: "advertisement/ad", locals: { ad: @advertisement } %> # # This will render the partial "advertisement/_ad.html.erb" regardless of which controller this is being called from. # @@ -88,32 +88,32 @@ module ActionView # and pick the proper path by checking `to_partial_path` method. # # # @account.to_partial_path returns 'accounts/account', so it can be used to replace: - # # <%= render :partial => "accounts/account", :locals => { :account => @account} %> - # <%= render :partial => @account %> + # # <%= render partial: "accounts/account", locals: { account: @account} %> + # <%= render partial: @account %> # # # @posts is an array of Post instances, so every post record returns 'posts/post' on `to_partial_path`, # # that's why we can replace: - # # <%= render :partial => "posts/post", :collection => @posts %> - # <%= render :partial => @posts %> + # # <%= render partial: "posts/post", collection: @posts %> + # <%= render partial: @posts %> # # == Rendering the default case # # If you're not going to be using any of the options like collections or layouts, you can also use the short-hand # defaults of render to render partials. Examples: # - # # Instead of <%= render :partial => "account" %> + # # Instead of <%= render partial: "account" %> # <%= render "account" %> # - # # Instead of <%= render :partial => "account", :locals => { :account => @buyer } %> - # <%= render "account", :account => @buyer %> + # # Instead of <%= render partial: "account", locals: { account: @buyer } %> + # <%= render "account", account: @buyer %> # # # @account.to_partial_path returns 'accounts/account', so it can be used to replace: - # # <%= render :partial => "accounts/account", :locals => { :account => @account} %> + # # <%= render partial: "accounts/account", locals: { account: @account} %> # <%= render @account %> # # # @posts is an array of Post instances, so every post record returns 'posts/post' on `to_partial_path`, # # that's why we can replace: - # # <%= render :partial => "posts/post", :collection => @posts %> + # # <%= render partial: "posts/post", collection: @posts %> # <%= render @posts %> # # == Rendering partials with layouts @@ -124,10 +124,10 @@ module ActionView # # <%# app/views/users/index.html.erb &> # Here's the administrator: - # <%= render :partial => "user", :layout => "administrator", :locals => { :user => administrator } %> + # <%= render partial: "user", layout: "administrator", locals: { user: administrator } %> # # Here's the editor: - # <%= render :partial => "user", :layout => "editor", :locals => { :user => editor } %> + # <%= render partial: "user", layout: "editor", locals: { user: editor } %> # # <%# app/views/users/_user.html.erb &> # Name: <%= user.name %> @@ -158,8 +158,8 @@ module ActionView # Name: <%= user.name %> # </div> # - # If a collection is given, the layout will be rendered once for each item in the collection. Just think - # these two snippets have the same output: + # If a collection is given, the layout will be rendered once for each item in + # the collection. Just think these two snippets have the same output: # # <%# app/views/users/_user.html.erb %> # Name: <%= user.name %> @@ -169,7 +169,7 @@ module ActionView # <ul> # <% users.each do |user| -%> # <li> - # <%= render :partial => "user", :locals => { :user => user } %> + # <%= render partial: "user", locals: { user: user } %> # </li> # <% end -%> # </ul> @@ -181,10 +181,10 @@ module ActionView # # <%# app/views/users/index.html.erb %> # <ul> - # <%= render :partial => "user", :layout => "li_layout", :collection => users %> + # <%= render partial: "user", layout: "li_layout", collection: users %> # </ul> # - # Given two users whose names are Alice and Bob, these snippets return: + # Given two users whose names are Alice and Bob, these snippets return: # # <ul> # <li> @@ -195,10 +195,14 @@ module ActionView # </li> # </ul> # + # The current object being rendered, as well as the object_counter, will be + # available as local variables inside the layout template under the same names + # as available in the partial. + # # You can also apply a layout to a block within any template: # # <%# app/views/users/_chief.html.erb &> - # <%= render(:layout => "administrator", :locals => { :user => chief }) do %> + # <%= render(layout: "administrator", locals: { user: chief }) do %> # Title: <%= chief.title %> # <% end %> # @@ -221,7 +225,7 @@ module ActionView # </div> # # <%# app/views/users/index.html.erb &> - # <%= render :layout => @users do |user| %> + # <%= render layout: @users do |user| %> # Title: <%= user.title %> # <% end %> # @@ -237,7 +241,7 @@ module ActionView # </div> # # <%# app/views/users/index.html.erb &> - # <%= render :layout => @users do |user, section| %> + # <%= render layout: @users do |user, section| %> # <%- case section when :header -%> # Title: <%= user.title %> # <%- when :footer -%> @@ -245,18 +249,27 @@ module ActionView # <%- end -%> # <% end %> class PartialRenderer < AbstractRenderer - PARTIAL_NAMES = Hash.new { |h,k| h[k] = {} } + PREFIXED_PARTIAL_NAMES = ThreadSafe::Cache.new do |h, k| + h[k] = ThreadSafe::Cache.new + end def initialize(*) super @context_prefix = @lookup_context.prefixes.first - @partial_names = PARTIAL_NAMES[@context_prefix] end def render(context, options, block) setup(context, options, block) identifier = (@template = find_partial) ? @template.identifier : @path + @lookup_context.rendered_format ||= begin + if @template && @template.formats.present? + @template.formats.first + else + formats.first + end + end + if @collection instrument(:collection, :identifier => identifier || "collection", :count => @collection.size) do render_collection @@ -272,26 +285,19 @@ module ActionView return nil if @collection.blank? if @options.key?(:spacer_template) - spacer = find_template(@options[:spacer_template]).render(@view, @locals) - end - - if layout = @options[:layout] - layout = find_template(layout) + spacer = find_template(@options[:spacer_template], @locals.keys).render(@view, @locals) end result = @template ? collection_with_template : collection_without_template - - result.map!{|content| layout.render(@view, @locals) { content } } if layout - result.join(spacer).html_safe end def render_partial - locals, view, block = @locals, @view, @block + view, locals, block = @view, @locals, @block object, as = @object, @variable if !block && (layout = @options[:layout]) - layout = find_template(layout) + layout = find_template(layout.to_s, @template_keys) end object ||= locals[as] @@ -316,7 +322,7 @@ module ActionView @block = block @details = extract_details(options) - @lookup_context.rendered_format ||= formats.first + prepend_formats(options[:formats]) if String === partial @object = options[:object] @@ -333,16 +339,16 @@ module ActionView end end - if @path - @variable, @variable_counter = retrieve_variable(@path) - else - paths.map! { |path| retrieve_variable(path).unshift(path) } + if as = options[:as] + raise_invalid_identifier(as) unless as.to_s =~ /\A[a-z_]\w*\z/ + as = as.to_sym end - if String === partial && @variable.to_s !~ /^[a-z_][a-zA-Z_0-9]*$/ - raise ArgumentError.new("The partial name (#{partial}) is not a valid Ruby identifier; " + - "make sure your partial name starts with a letter or underscore, " + - "and is followed by any combinations of letters, numbers, or underscores.") + if @path + @variable, @variable_counter = retrieve_variable(@path, as) + @template_keys = retrieve_template_keys + else + paths.map! { |path| retrieve_variable(path, as).unshift(path) } end self @@ -356,56 +362,55 @@ module ActionView end def collection_from_object - if @object.respond_to?(:to_ary) - @object.to_ary - end + @object.to_ary if @object.respond_to?(:to_ary) end def find_partial if path = @path - locals = @locals.keys - locals << @variable - locals << @variable_counter if @collection - find_template(path, locals) + find_template(path, @template_keys) end end - def find_template(path=@path, locals=@locals.keys) + def find_template(path, locals) prefixes = path.include?(?/) ? [] : @lookup_context.prefixes @lookup_context.find_template(path, prefixes, true, locals, @details) end def collection_with_template - segments, locals, template = [], @locals, @template + view, locals, template = @view, @locals, @template as, counter = @variable, @variable_counter - locals[counter] = -1 + if layout = @options[:layout] + layout = find_template(layout, @template_keys) + end + + index = -1 + @collection.map do |object| + locals[as] = object + locals[counter] = (index += 1) - @collection.each do |object| - locals[counter] += 1 - locals[as] = object - segments << template.render(@view, locals) + content = template.render(view, locals) + content = layout.render(view, locals) { content } if layout + content end - - segments end - def collection_without_template - segments, locals, collection_data = [], @locals, @collection_data - index, template, cache = -1, nil, {} - keys = @locals.keys + view, locals, collection_data = @view, @locals, @collection_data + cache = {} + keys = @locals.keys - @collection.each_with_index do |object, i| - path, *data = collection_data[i] - template = (cache[path] ||= find_template(path, keys + data)) - locals[data[0]] = object - locals[data[1]] = (index += 1) - segments << template.render(@view, locals) - end + index = -1 + @collection.map do |object| + index += 1 + path, as, counter = collection_data[index] + + locals[as] = object + locals[counter] = index - @template = template - segments + template = (cache[path] ||= find_template(path, keys + [as, counter])) + template.render(view, locals) + end end def partial_path(object = @object) @@ -417,7 +422,15 @@ module ActionView raise ArgumentError.new("'#{object.inspect}' is not an ActiveModel-compatible object. It must implement :to_partial_path.") end - @partial_names[path] ||= merge_prefix_into_object_path(@context_prefix, path.dup) + if @view.prefix_partial_path_with_controller_namespace + prefixed_partial_names[path] ||= merge_prefix_into_object_path(@context_prefix, path.dup) + else + path + end + end + + def prefixed_partial_names + @prefixed_partial_names ||= PREFIXED_PARTIAL_NAMES[@context_prefix] end def merge_prefix_into_object_path(prefix, object_path) @@ -437,10 +450,29 @@ module ActionView end end - def retrieve_variable(path) - variable = @options[:as].try(:to_sym) || path[%r'_?(\w+)(\.\w+)*$', 1].to_sym + def retrieve_template_keys + keys = @locals.keys + keys << @variable + keys << @variable_counter if @collection + keys + end + + def retrieve_variable(path, as) + variable = as || begin + base = path[-1] == "/" ? "" : File.basename(path) + raise_invalid_identifier(path) unless base =~ /\A_?([a-z]\w*)(\.\w+)*\z/ + $1.to_sym + end variable_counter = :"#{variable}_counter" if @collection [variable, variable_counter] end + + IDENTIFIER_ERROR_MESSAGE = "The partial name (%s) is not a valid Ruby identifier; " + + "make sure your partial name starts with a lowercase letter or underscore, " + + "and is followed by any combination of letters, numbers and underscores." + + def raise_invalid_identifier(path) + raise ArgumentError.new(IDENTIFIER_ERROR_MESSAGE % (path)) + end end end |