aboutsummaryrefslogtreecommitdiffstats
path: root/actionpack/lib/action_view/renderer
diff options
context:
space:
mode:
Diffstat (limited to 'actionpack/lib/action_view/renderer')
-rw-r--r--actionpack/lib/action_view/renderer/abstract_renderer.rb5
-rw-r--r--actionpack/lib/action_view/renderer/partial_renderer.rb290
-rw-r--r--actionpack/lib/action_view/renderer/renderer.rb54
-rw-r--r--actionpack/lib/action_view/renderer/streaming_template_renderer.rb62
-rw-r--r--actionpack/lib/action_view/renderer/template_renderer.rb8
5 files changed, 334 insertions, 85 deletions
diff --git a/actionpack/lib/action_view/renderer/abstract_renderer.rb b/actionpack/lib/action_view/renderer/abstract_renderer.rb
index 4a52b3172e..60c527beeb 100644
--- a/actionpack/lib/action_view/renderer/abstract_renderer.rb
+++ b/actionpack/lib/action_view/renderer/abstract_renderer.rb
@@ -3,9 +3,8 @@ module ActionView
delegate :find_template, :template_exists?, :with_fallbacks, :update_details,
:with_layout_format, :formats, :freeze_formats, :to => :@lookup_context
- def initialize(view)
- @view = view
- @lookup_context = view.lookup_context
+ def initialize(lookup_context)
+ @lookup_context = lookup_context
end
def render
diff --git a/actionpack/lib/action_view/renderer/partial_renderer.rb b/actionpack/lib/action_view/renderer/partial_renderer.rb
index 10cd37d56f..a351fbc04f 100644
--- a/actionpack/lib/action_view/renderer/partial_renderer.rb
+++ b/actionpack/lib/action_view/renderer/partial_renderer.rb
@@ -1,44 +1,230 @@
+require 'active_support/core_ext/object/blank'
+
module ActionView
+ # = Action View Partials
+ #
+ # There's also a convenience method for rendering sub templates within the current controller that depends on a
+ # single object (we call this kind of sub templates for partials). It relies on the fact that partials should
+ # follow the naming convention of being prefixed with an underscore -- as to separate them from regular
+ # templates that could be rendered on their own.
+ #
+ # In a template for Advertiser#account:
+ #
+ # <%= render :partial => "account" %>
+ #
+ # This would render "advertiser/_account.html.erb" and pass the instance variable @account in as a local variable
+ # +account+ to the template for display.
+ #
+ # In another template for Advertiser#buy, we could have:
+ #
+ # <%= render :partial => "account", :locals => { :account => @buyer } %>
+ #
+ # <% @advertisements.each do |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
+ # render "advertiser/_ad.html.erb" and pass the local variable +ad+ to the template for display.
+ #
+ # == The :as and :object options
+ #
+ # By default <tt>ActionView::Partials::PartialRenderer</tt> has its object in a local variable with the same
+ # name as the template. So, given
+ #
+ # <%= render :partial => "contract" %>
+ #
+ # within contract we'll get <tt>@contract</tt> in the local variable +contract+, as if we had written
+ #
+ # <%= render :partial => "contract", :locals => { :contract => @contract } %>
+ #
+ # With the <tt>:as</tt> option we can specify a different name for said local variable. For example, if we
+ # wanted it to be +agreement+ instead of +contract+ we'd do:
+ #
+ # <%= render :partial => "contract", :as => 'agreement' %>
+ #
+ # The <tt>:object</tt> option can be used to directly specify which object is rendered into the partial;
+ # useful when the template's object is elsewhere, in a different ivar or in a local variable for instance.
+ #
+ # Revisiting a previous example we could have written this code:
+ #
+ # <%= render :partial => "account", :object => @buyer %>
+ #
+ # <% @advertisements.each do |ad| %>
+ # <%= render :partial => "ad", :object => ad %>
+ # <% end %>
+ #
+ # The <tt>:object</tt> and <tt>:as</tt> options can be used together.
+ #
+ # == Rendering a collection of partials
+ #
+ # The example of partial use describes a familiar pattern where a template needs to iterate over an array and
+ # render a sub template for each of the elements. This pattern has been implemented as a single method that
+ # 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 %>
+ #
+ # 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
+ # +partial_name_counter+. In the case of the example above, the template would be fed +ad_counter+.
+ #
+ # The <tt>:as</tt> option may be used when rendering partials.
+ #
+ # 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" %>
+ #
+ # 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" %>
+ #
+ # 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.
+ #
+ # == Rendering shared partials
+ #
+ # Two controllers can share a set of partials and render them like this:
+ #
+ # <%= 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.
+ #
+ # == Rendering objects with the RecordIdentifier
+ #
+ # Instead of explicitly naming the location of a partial, you can also let the RecordIdentifier do the work if
+ # you're following its conventions for RecordIdentifier#partial_path. Examples:
+ #
+ # # @account is an Account instance, so it uses the RecordIdentifier to replace
+ # # <%= render :partial => "accounts/account", :locals => { :account => @account} %>
+ # <%= render :partial => @account %>
+ #
+ # # @posts is an array of Post instances, so it uses the RecordIdentifier to replace
+ # # <%= 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" %>
+ # <%= render "account" %>
+ #
+ # # Instead of <%= render :partial => "account", :locals => { :account => @buyer } %>
+ # <%= render "account", :account => @buyer %>
+ #
+ # # @account is an Account instance, so it uses the RecordIdentifier to replace
+ # # <%= render :partial => "accounts/account", :locals => { :account => @account } %>
+ # <%= render(@account) %>
+ #
+ # # @posts is an array of Post instances, so it uses the RecordIdentifier to replace
+ # # <%= render :partial => "posts/post", :collection => @posts %>
+ # <%= render(@posts) %>
+ #
+ # == Rendering partials with layouts
+ #
+ # Partials can have their own layouts applied to them. These layouts are different than the ones that are
+ # specified globally for the entire action, but they work in a similar fashion. Imagine a list with two types
+ # of users:
+ #
+ # <%# app/views/users/index.html.erb &>
+ # Here's the administrator:
+ # <%= render :partial => "user", :layout => "administrator", :locals => { :user => administrator } %>
+ #
+ # Here's the editor:
+ # <%= render :partial => "user", :layout => "editor", :locals => { :user => editor } %>
+ #
+ # <%# app/views/users/_user.html.erb &>
+ # Name: <%= user.name %>
+ #
+ # <%# app/views/users/_administrator.html.erb &>
+ # <div id="administrator">
+ # Budget: $<%= user.budget %>
+ # <%= yield %>
+ # </div>
+ #
+ # <%# app/views/users/_editor.html.erb &>
+ # <div id="editor">
+ # Deadline: <%= user.deadline %>
+ # <%= yield %>
+ # </div>
+ #
+ # ...this will return:
+ #
+ # Here's the administrator:
+ # <div id="administrator">
+ # Budget: $<%= user.budget %>
+ # Name: <%= user.name %>
+ # </div>
+ #
+ # Here's the editor:
+ # <div id="editor">
+ # Deadline: <%= user.deadline %>
+ # Name: <%= user.name %>
+ # </div>
+ #
+ # 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 %>
+ # Title: <%= chief.title %>
+ # <% end %>
+ #
+ # ...this will return:
+ #
+ # <div id="administrator">
+ # Budget: $<%= user.budget %>
+ # Title: <%= chief.name %>
+ # </div>
+ #
+ # As you can see, the <tt>:locals</tt> hash is shared between both the partial and its layout.
+ #
+ # If you pass arguments to "yield" then this will be passed to the block. One way to use this is to pass
+ # an array to layout and treat it as an enumerable.
+ #
+ # <%# app/views/users/_user.html.erb &>
+ # <div class="user">
+ # Budget: $<%= user.budget %>
+ # <%= yield user %>
+ # </div>
+ #
+ # <%# app/views/users/index.html.erb &>
+ # <%= render :layout => @users do |user| %>
+ # Title: <%= user.title %>
+ # <% end %>
+ #
+ # This will render the layout for each user and yield to the block, passing the user, each time.
+ #
+ # You can also yield multiple times in one layout and use block arguments to differentiate the sections.
+ #
+ # <%# app/views/users/_user.html.erb &>
+ # <div class="user">
+ # <%= yield user, :header %>
+ # Budget: $<%= user.budget %>
+ # <%= yield user, :footer %>
+ # </div>
+ #
+ # <%# app/views/users/index.html.erb &>
+ # <%= render :layout => @users do |user, section| %>
+ # <%- case section when :header -%>
+ # Title: <%= user.title %>
+ # <%- when :footer -%>
+ # Deadline: <%= user.deadline %>
+ # <%- end -%>
+ # <% end %>
class PartialRenderer < AbstractRenderer #:nodoc:
PARTIAL_NAMES = Hash.new {|h,k| h[k] = {} }
- def initialize(view)
+ def initialize(*)
super
- @partial_names = PARTIAL_NAMES[@view.controller.class.name]
+ @partial_names = PARTIAL_NAMES[@lookup_context.prefixes.first]
end
- def setup(options, block)
- partial = options[:partial]
-
- @options = options
- @locals = options[:locals] || {}
- @block = block
-
- if String === partial
- @object = options[:object]
- @path = partial
- @collection = collection
- else
- @object = partial
-
- if @collection = collection_from_object || collection
- paths = @collection_data = @collection.map { |o| partial_path(o) }
- @path = paths.uniq.size == 1 ? paths.first : nil
- else
- @path = partial_path
- end
- end
-
- if @path
- @variable, @variable_counter = retrieve_variable(@path)
- else
- paths.map! { |path| retrieve_variable(path).unshift(path) }
- end
-
- self
- end
+ def render(context, options, block)
+ setup(context, options, block)
- def render
wrap_formats(@path) do
identifier = ((@template = find_partial) ? @template.identifier : @path)
@@ -77,7 +263,7 @@ module ActionView
locals[as] = object
content = @template.render(view, locals) do |*name|
- view._block_layout_for(*name, &block)
+ view._layout_for(*name, &block)
end
content = layout.render(view, locals){ content } if layout
@@ -86,6 +272,38 @@ module ActionView
private
+ def setup(context, options, block)
+ @view = context
+ partial = options[:partial]
+
+ @options = options
+ @locals = options[:locals] || {}
+ @block = block
+
+ if String === partial
+ @object = options[:object]
+ @path = partial
+ @collection = collection
+ else
+ @object = partial
+
+ if @collection = collection_from_object || collection
+ paths = @collection_data = @collection.map { |o| partial_path(o) }
+ @path = paths.uniq.size == 1 ? paths.first : nil
+ else
+ @path = partial_path
+ end
+ end
+
+ if @path
+ @variable, @variable_counter = retrieve_variable(@path)
+ else
+ paths.map! { |path| retrieve_variable(path).unshift(path) }
+ end
+
+ self
+ end
+
def collection
if @options.key?(:collection)
collection = @options[:collection]
@@ -109,7 +327,7 @@ module ActionView
end
def find_template(path=@path, locals=@locals.keys)
- prefixes = path.include?(?/) ? [] : @view.controller_prefixes
+ prefixes = path.include?(?/) ? [] : @lookup_context.prefixes
@lookup_context.find_template(path, prefixes, true, locals)
end
@@ -150,7 +368,7 @@ module ActionView
object = object.to_model if object.respond_to?(:to_model)
object.class.model_name.partial_path.dup.tap do |partial|
- path = @view.controller_prefixes.first
+ path = @lookup_context.prefixes.first
partial.insert(0, "#{File.dirname(path)}/") if partial.include?(?/) && path.include?(?/)
end
end
diff --git a/actionpack/lib/action_view/renderer/renderer.rb b/actionpack/lib/action_view/renderer/renderer.rb
new file mode 100644
index 0000000000..bf1b5a7d22
--- /dev/null
+++ b/actionpack/lib/action_view/renderer/renderer.rb
@@ -0,0 +1,54 @@
+module ActionView
+ # This is the main entry point for rendering. It basically delegates
+ # to other objects like TemplateRenderer and PartialRenderer which
+ # actually renders the template.
+ class Renderer
+ attr_accessor :lookup_context
+
+ def initialize(lookup_context)
+ @lookup_context = lookup_context
+ end
+
+ # Main render entry point shared by AV and AC.
+ def render(context, options)
+ if options.key?(:partial)
+ render_partial(context, options)
+ else
+ render_template(context, options)
+ end
+ end
+
+ # Render but returns a valid Rack body. If fibers are defined, we return
+ # a streaming body that renders the template piece by piece.
+ #
+ # Note that partials are not supported to be rendered with streaming,
+ # so in such cases, we just wrap them in an array.
+ def render_body(context, options)
+ if options.key?(:partial)
+ [render_partial(context, options)]
+ else
+ StreamingTemplateRenderer.new(@lookup_context).render(context, options)
+ end
+ end
+
+ # Direct accessor to template rendering.
+ def render_template(context, options) #:nodoc:
+ _template_renderer.render(context, options)
+ end
+
+ # Direct access to partial rendering.
+ def render_partial(context, options, &block) #:nodoc:
+ _partial_renderer.render(context, options, block)
+ end
+
+ private
+
+ def _template_renderer #:nodoc:
+ @_template_renderer ||= TemplateRenderer.new(@lookup_context)
+ end
+
+ def _partial_renderer #:nodoc:
+ @_partial_renderer ||= PartialRenderer.new(@lookup_context)
+ end
+ end
+end
diff --git a/actionpack/lib/action_view/renderer/streaming_template_renderer.rb b/actionpack/lib/action_view/renderer/streaming_template_renderer.rb
index 03aab444f8..1ccf5a8ddb 100644
--- a/actionpack/lib/action_view/renderer/streaming_template_renderer.rb
+++ b/actionpack/lib/action_view/renderer/streaming_template_renderer.rb
@@ -4,50 +4,11 @@
require 'fiber' if defined?(Fiber)
module ActionView
- # Consider the following layout:
- #
- # <%= yield :header %>
- # 2
- # <%= yield %>
- # 5
- # <%= yield :footer %>
- #
- # And template:
- #
- # <%= provide :header, "1" %>
- # 3
- # 4
- # <%= provide :footer, "6" %>
- #
- # It will stream:
- #
- # "1\n", "2\n", "3\n4\n", "5\n", "6\n"
- #
- # Notice that once you <%= yield %>, it will render the whole template
- # before streaming again. In the future, we can also support streaming
- # from the template and not only the layout.
- #
- # Also, notice we use +provide+ instead of +content_for+, as +provide+
- # gives the control back to the layout as soon as it is called.
- # With +content_for+, it would render all the template to find all
- # +content_for+ calls. For instance, consider this layout:
- #
- # <%= yield :header %>
- #
- # With this template:
- #
- # <%= content_for :header, "1" %>
- # <%= provide :header, "2" %>
- # <%= provide :header, "3" %>
- #
- # It will return "12\n" because +content_for+ continues rendering the
- # template but it is returns back to the layout as soon as it sees the
- # first +provide+.
- #
# == TODO
#
# * Support streaming from child templates, partials and so on.
# * Integrate exceptions with exceptron
+ # * Rack::Cache needs to support streaming bodies
class StreamingTemplateRenderer < TemplateRenderer #:nodoc:
# A valid Rack::Body (i.e. it responds to each).
# It is initialized with a block that, when called, starts
@@ -60,11 +21,26 @@ module ActionView
def each(&block)
begin
@start.call(block)
- rescue
+ rescue Exception => exception
+ log_error(exception)
block.call ActionView::Base.streaming_completion_on_exception
end
self
end
+
+ private
+
+ # This is the same logging logic as in ShowExceptions middleware.
+ # TODO Once "exceptron" is in, refactor this piece to simply re-use exceptron.
+ def log_error(exception) #:nodoc:
+ logger = ActionController::Base.logger
+ return unless logger
+
+ message = "\n#{exception.class} (#{exception.message}):\n"
+ message << exception.annoted_source_code.to_s if exception.respond_to?(:annoted_source_code)
+ message << " " << exception.backtrace.join("\n ")
+ logger.fatal("#{message}\n\n")
+ end
end
# For streaming, instead of rendering a given a template, we return a Body
@@ -106,7 +82,7 @@ module ActionView
# Set the view flow to support streaming. It will be aware
# when to stop rendering the layout because it needs to search
# something in the template and vice-versa.
- view._view_flow = StreamingFlow.new(view, fiber)
+ view.view_flow = StreamingFlow.new(view, fiber)
# Yo! Start the fiber!
fiber.resume
@@ -118,7 +94,7 @@ module ActionView
content = template.render(view, locals, &yielder)
# Once rendering the template is done, sets its content in the :layout key.
- view._view_flow.set(:layout, content)
+ view.view_flow.set(:layout, content)
# In case the layout continues yielding, we need to resume
# the fiber until all yields are handled.
diff --git a/actionpack/lib/action_view/renderer/template_renderer.rb b/actionpack/lib/action_view/renderer/template_renderer.rb
index 6b5ead463f..a09cef8fef 100644
--- a/actionpack/lib/action_view/renderer/template_renderer.rb
+++ b/actionpack/lib/action_view/renderer/template_renderer.rb
@@ -3,7 +3,9 @@ require 'active_support/core_ext/array/wrap'
module ActionView
class TemplateRenderer < AbstractRenderer #:nodoc:
- def render(options)
+ def render(context, options)
+ @view = context
+
wrap_formats(options[:template] || options[:file]) do
template = determine_template(options)
freeze_formats(template.formats, true)
@@ -18,7 +20,7 @@ module ActionView
if options.key?(:text)
Template::Text.new(options[:text], formats.try(:first))
elsif options.key?(:file)
- with_fallbacks { find_template(options[:file], options[:prefixes], false, keys) }
+ with_fallbacks { find_template(options[:file], nil, false, keys) }
elsif options.key?(:inline)
handler = Template.handler_for_extension(options[:type] || "erb")
Template.new(options[:inline], "inline template", handler, :locals => keys)
@@ -46,7 +48,7 @@ module ActionView
if layout
view = @view
- view._view_flow.set(:layout, content)
+ view.view_flow.set(:layout, content)
layout.render(view, locals){ |*name| view._layout_for(*name) }
else
content