Skip to content

Commit

Permalink
Add support to render :once.
Browse files Browse the repository at this point in the history
This will be used internally by sprockets to ensure requires are executed just once.
  • Loading branch information
josevalim committed Oct 10, 2010
1 parent b88f4ca commit 940b577
Show file tree
Hide file tree
Showing 6 changed files with 139 additions and 29 deletions.
2 changes: 1 addition & 1 deletion actionpack/lib/abstract_controller/rendering.rb
Expand Up @@ -155,7 +155,7 @@ def _normalize_options(options)
options[:partial] = action_name options[:partial] = action_name
end end


if (options.keys & [:partial, :file, :template]).empty? if (options.keys & [:partial, :file, :template, :once]).empty?
options[:prefix] ||= _prefix options[:prefix] ||= _prefix
end end


Expand Down
7 changes: 7 additions & 0 deletions actionpack/lib/action_view/render/rendering.rb
Expand Up @@ -10,6 +10,7 @@ module Rendering
# * <tt>:file</tt> - Renders an explicit template file (this used to be the old default), add :locals to pass in those. # * <tt>:file</tt> - Renders an explicit template file (this used to be the old default), add :locals to pass in those.
# * <tt>:inline</tt> - Renders an inline template similar to how it's done in the controller. # * <tt>:inline</tt> - Renders an inline template similar to how it's done in the controller.
# * <tt>:text</tt> - Renders the text passed in out. # * <tt>:text</tt> - Renders the text passed in out.
# * <tt>:once</tt> - Receives :template paths and ensures they are rendered just once.
# #
# If no options hash is passed or :update specified, the default is to render a partial and use the second parameter # If no options hash is passed or :update specified, the default is to render a partial and use the second parameter
# as the locals hash. # as the locals hash.
Expand All @@ -20,6 +21,8 @@ def render(options = {}, locals = {}, &block)
_render_partial(options.merge(:partial => options[:layout]), &block) _render_partial(options.merge(:partial => options[:layout]), &block)
elsif options.key?(:partial) elsif options.key?(:partial)
_render_partial(options) _render_partial(options)
elsif options.key?(:once)
_render_once(options)
else else
_render_template(options) _render_template(options)
end end
Expand Down Expand Up @@ -88,6 +91,10 @@ def _layout_for(*args, &block)
end end
end end


def _render_once(options) #:nodoc:
_template_renderer.render_once(options)
end

def _render_template(options) #:nodoc: def _render_template(options) #:nodoc:
_template_renderer.render(options) _template_renderer.render(options)
end end
Expand Down
12 changes: 6 additions & 6 deletions actionpack/lib/action_view/renderer/abstract_renderer.rb
Expand Up @@ -14,12 +14,6 @@ def render
raise NotImplementedError raise NotImplementedError
end end


# Contains the logic that actually renders the layout.
def render_layout(layout, locals, &block) #:nodoc:
view = @view
layout.render(view, locals){ |*name| view._layout_for(*name, &block) }
end

# Checks if the given path contains a format and if so, change # Checks if the given path contains a format and if so, change
# the lookup context to take this new format into account. # the lookup context to take this new format into account.
def wrap_formats(value) def wrap_formats(value)
Expand All @@ -32,5 +26,11 @@ def wrap_formats(value)
yield yield
end end
end end

protected

def instrument(name, options={})
ActiveSupport::Notifications.instrument("render_#{name}.action_view", options){ yield }
end
end end
end end
7 changes: 3 additions & 4 deletions actionpack/lib/action_view/renderer/partial_renderer.rb
Expand Up @@ -2,7 +2,6 @@


module ActionView module ActionView
class PartialRenderer < AbstractRenderer #:nodoc: class PartialRenderer < AbstractRenderer #:nodoc:
N = ::ActiveSupport::Notifications
PARTIAL_NAMES = Hash.new {|h,k| h[k] = {} } PARTIAL_NAMES = Hash.new {|h,k| h[k] = {} }


def initialize(view) def initialize(view)
Expand Down Expand Up @@ -46,11 +45,11 @@ def render
identifier = ((@template = find_partial) ? @template.identifier : @path) identifier = ((@template = find_partial) ? @template.identifier : @path)


if @collection if @collection
N.instrument("render_collection.action_view", :identifier => identifier || "collection", :count => @collection.size) do instrument(:collection, :identifier => identifier || "collection", :count => @collection.size) do
render_collection render_collection
end end
else else
N.instrument("render_partial.action_view", :identifier => identifier) do instrument(:partial, :identifier => identifier) do
render_partial render_partial
end end
end end
Expand Down Expand Up @@ -83,7 +82,7 @@ def render_partial
view._layout_for(*name, &block) view._layout_for(*name, &block)
end end


content = render_layout(layout, locals){ content } if layout content = layout.render(view, locals){ content } if layout
content content
end end


Expand Down
67 changes: 49 additions & 18 deletions actionpack/lib/action_view/renderer/template_renderer.rb
@@ -1,26 +1,51 @@
require 'set'
require 'active_support/core_ext/object/try'
require 'active_support/core_ext/array/wrap'
require 'action_view/renderer/abstract_renderer' require 'action_view/renderer/abstract_renderer'


module ActionView module ActionView
class TemplateRenderer < AbstractRenderer #:nodoc: class TemplateRenderer < AbstractRenderer #:nodoc:
attr_reader :rendered

def initialize(view)
super
@rendered = Set.new
end

def render(options) def render(options)
wrap_formats(options[:template] || options[:file]) do wrap_formats(options[:template] || options[:file]) do
template = determine_template(options) template = determine_template(options)
lookup_context.freeze_formats(template.formats, true) render_template(template, options[:layout], options[:locals])
render_template(template, options[:layout], options) end
end

def render_once(options)
paths, locals = options[:once], options[:locals] || {}
layout, keys, prefix = options[:layout], locals.keys, options[:prefix]

raise "render :once expects a String or an Array to be given" unless paths

render_with_layout(layout, locals) do
contents = []
Array.wrap(paths).each do |path|
template = find_template(path, prefix, false, keys)
contents << render_template(template, nil, locals) if @rendered.add?(template)
end
contents.join("\n")
end end
end end


# Determine the template to be rendered using the given options. # Determine the template to be rendered using the given options.
def determine_template(options) #:nodoc: def determine_template(options) #:nodoc:
keys = (options[:locals] ||= {}).keys keys = options[:locals].try(:keys) || []


if options.key?(:inline) if options.key?(:text)
handler = Template.handler_class_for_extension(options[:type] || "erb")
Template.new(options[:inline], "inline template", handler, { :locals => keys })
elsif options.key?(:text)
Template::Text.new(options[:text], formats.try(:first)) Template::Text.new(options[:text], formats.try(:first))
elsif options.key?(:file) elsif options.key?(:file)
with_fallbacks { find_template(options[:file], options[:prefix], false, keys) } with_fallbacks { find_template(options[:file], options[:prefix], false, keys) }
elsif options.key?(:inline)
handler = Template.handler_class_for_extension(options[:type] || "erb")
Template.new(options[:inline], "inline template", handler, { :locals => keys })
elsif options.key?(:template) elsif options.key?(:template)
options[:template].respond_to?(:render) ? options[:template].respond_to?(:render) ?
options[:template] : find_template(options[:template], options[:prefix], false, keys) options[:template] : find_template(options[:template], options[:prefix], false, keys)
Expand All @@ -29,20 +54,26 @@ def determine_template(options) #:nodoc:


# Renders the given template. An string representing the layout can be # Renders the given template. An string representing the layout can be
# supplied as well. # supplied as well.
def render_template(template, layout = nil, options = {}) #:nodoc: def render_template(template, layout_name = nil, locals = {}) #:nodoc:
view, locals = @view, options[:locals] || {} lookup_context.freeze_formats(template.formats, true)
layout = find_layout(layout, locals.keys) if layout view, locals = @view, locals || {}


ActiveSupport::Notifications.instrument("render_template.action_view", render_with_layout(layout_name, locals) do |layout|
:identifier => template.identifier, :layout => layout.try(:virtual_path)) do instrument(:template, :identifier => template.identifier, :layout => layout.try(:virtual_path)) do

template.render(view, locals) { |*name| view._layout_for(*name) }
content = template.render(view, locals) { |*name| view._layout_for(*name) }

if layout
view.store_content_for(:layout, content)
content = render_layout(layout, locals)
end end
end
end

def render_with_layout(path, locals) #:nodoc:
layout = path && find_layout(path, locals.keys)
content = yield(layout)


if layout
view = @view
view.store_content_for(:layout, content)
layout.render(view, locals){ |*name| view._layout_for(*name) }
else
content content
end end
end end
Expand Down
73 changes: 73 additions & 0 deletions actionpack/test/controller/new_base/render_once_test.rb
@@ -0,0 +1,73 @@
require 'abstract_unit'

module RenderTemplate
class RenderOnceController < ActionController::Base
layout false

RESOLVER = ActionView::FixtureResolver.new(
"test/a.html.erb" => "a",
"test/b.html.erb" => "<>",
"test/c.html.erb" => "c",
"test/one.html.erb" => "<%= render :once => 'test/result' %>",
"test/two.html.erb" => "<%= render :once => 'test/result' %>",
"test/three.html.erb" => "<%= render :once => 'test/result' %>",
"test/result.html.erb" => "YES!",
"layouts/test.html.erb" => "l<%= yield %>l"
)

self.view_paths = [RESOLVER]

def multiple
render :once => %w(test/a test/b test/c)
end

def once
render :once => %w(test/one test/two test/three)
end

def duplicate
render :once => %w(test/a test/a test/a)
end

def with_layout
render :once => %w(test/a test/b test/c), :layout => "test"
end
end

module Tests
def test_mutliple_arguments_get_all_rendered
get :multiple
assert_response "a\n<>\nc"
end

def test_referenced_templates_get_rendered_once
get :once
assert_response "YES!\n\n"
end

def test_duplicated_templates_get_rendered_once
get :duplicate
assert_response "a"
end

def test_layout_wraps_all_rendered_templates
get :with_layout
assert_response "la\n<>\ncl"
end
end

class TestWithResolverCache < Rack::TestCase
testing RenderTemplate::RenderOnceController
include Tests
end

# TODO This still needs to be implemented and supported.
# class TestWithoutResolverCache < Rack::TestCase
# testing RenderTemplate::RenderOnceController
# include Tests
#
# def setup
# RenderTemplate::RenderOnceController::RESOLVER.stubs(:caching?).returns(false)
# end
# end
end

0 comments on commit 940b577

Please sign in to comment.