diff --git a/actionpack/lib/abstract_controller/rendering.rb b/actionpack/lib/abstract_controller/rendering.rb index e88e5aefc067c..1c63fb2d149e3 100644 --- a/actionpack/lib/abstract_controller/rendering.rb +++ b/actionpack/lib/abstract_controller/rendering.rb @@ -155,7 +155,7 @@ def _normalize_options(options) options[:partial] = action_name end - if (options.keys & [:partial, :file, :template]).empty? + if (options.keys & [:partial, :file, :template, :once]).empty? options[:prefix] ||= _prefix end diff --git a/actionpack/lib/action_view/render/rendering.rb b/actionpack/lib/action_view/render/rendering.rb index adbb6bc62636d..0cd5d9d437ebd 100644 --- a/actionpack/lib/action_view/render/rendering.rb +++ b/actionpack/lib/action_view/render/rendering.rb @@ -10,6 +10,7 @@ module Rendering # * :file - Renders an explicit template file (this used to be the old default), add :locals to pass in those. # * :inline - Renders an inline template similar to how it's done in the controller. # * :text - Renders the text passed in out. + # * :once - 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 # as the locals hash. @@ -20,6 +21,8 @@ def render(options = {}, locals = {}, &block) _render_partial(options.merge(:partial => options[:layout]), &block) elsif options.key?(:partial) _render_partial(options) + elsif options.key?(:once) + _render_once(options) else _render_template(options) end @@ -88,6 +91,10 @@ def _layout_for(*args, &block) end end + def _render_once(options) #:nodoc: + _template_renderer.render_once(options) + end + def _render_template(options) #:nodoc: _template_renderer.render(options) end diff --git a/actionpack/lib/action_view/renderer/abstract_renderer.rb b/actionpack/lib/action_view/renderer/abstract_renderer.rb index f9fa63ce7feac..77cfa51dffa4e 100644 --- a/actionpack/lib/action_view/renderer/abstract_renderer.rb +++ b/actionpack/lib/action_view/renderer/abstract_renderer.rb @@ -14,12 +14,6 @@ def render raise NotImplementedError 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 # the lookup context to take this new format into account. def wrap_formats(value) @@ -32,5 +26,11 @@ def wrap_formats(value) yield end end + + protected + + def instrument(name, options={}) + ActiveSupport::Notifications.instrument("render_#{name}.action_view", options){ yield } + end end end \ No newline at end of file diff --git a/actionpack/lib/action_view/renderer/partial_renderer.rb b/actionpack/lib/action_view/renderer/partial_renderer.rb index 3be1702f9e9b4..eff425687be49 100644 --- a/actionpack/lib/action_view/renderer/partial_renderer.rb +++ b/actionpack/lib/action_view/renderer/partial_renderer.rb @@ -2,7 +2,6 @@ module ActionView class PartialRenderer < AbstractRenderer #:nodoc: - N = ::ActiveSupport::Notifications PARTIAL_NAMES = Hash.new {|h,k| h[k] = {} } def initialize(view) @@ -46,11 +45,11 @@ def render identifier = ((@template = find_partial) ? @template.identifier : @path) 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 end else - N.instrument("render_partial.action_view", :identifier => identifier) do + instrument(:partial, :identifier => identifier) do render_partial end end @@ -83,7 +82,7 @@ def render_partial view._layout_for(*name, &block) end - content = render_layout(layout, locals){ content } if layout + content = layout.render(view, locals){ content } if layout content end diff --git a/actionpack/lib/action_view/renderer/template_renderer.rb b/actionpack/lib/action_view/renderer/template_renderer.rb index 3acc68dcac310..9f9df153479ea 100644 --- a/actionpack/lib/action_view/renderer/template_renderer.rb +++ b/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' module ActionView class TemplateRenderer < AbstractRenderer #:nodoc: + attr_reader :rendered + + def initialize(view) + super + @rendered = Set.new + end + def render(options) wrap_formats(options[:template] || options[:file]) do template = determine_template(options) - lookup_context.freeze_formats(template.formats, true) - render_template(template, options[:layout], options) + render_template(template, options[:layout], options[:locals]) + 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 # Determine the template to be rendered using the given options. def determine_template(options) #:nodoc: - keys = (options[:locals] ||= {}).keys + keys = options[:locals].try(:keys) || [] - if options.key?(:inline) - handler = Template.handler_class_for_extension(options[:type] || "erb") - Template.new(options[:inline], "inline template", handler, { :locals => keys }) - elsif options.key?(:text) + if options.key?(:text) Template::Text.new(options[:text], formats.try(:first)) elsif options.key?(:file) 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) options[:template].respond_to?(:render) ? options[:template] : find_template(options[:template], options[:prefix], false, keys) @@ -29,20 +54,26 @@ def determine_template(options) #:nodoc: # Renders the given template. An string representing the layout can be # supplied as well. - def render_template(template, layout = nil, options = {}) #:nodoc: - view, locals = @view, options[:locals] || {} - layout = find_layout(layout, locals.keys) if layout + def render_template(template, layout_name = nil, locals = {}) #:nodoc: + lookup_context.freeze_formats(template.formats, true) + view, locals = @view, locals || {} - ActiveSupport::Notifications.instrument("render_template.action_view", - :identifier => template.identifier, :layout => layout.try(:virtual_path)) do - - content = template.render(view, locals) { |*name| view._layout_for(*name) } - - if layout - view.store_content_for(:layout, content) - content = render_layout(layout, locals) + render_with_layout(layout_name, locals) do |layout| + instrument(:template, :identifier => template.identifier, :layout => layout.try(:virtual_path)) do + template.render(view, locals) { |*name| view._layout_for(*name) } 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 end end diff --git a/actionpack/test/controller/new_base/render_once_test.rb b/actionpack/test/controller/new_base/render_once_test.rb new file mode 100644 index 0000000000000..12892b7255058 --- /dev/null +++ b/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