diff options
-rw-r--r-- | actionpack/CHANGELOG.md | 5 | ||||
-rw-r--r-- | actionpack/lib/action_controller.rb | 1 | ||||
-rw-r--r-- | actionpack/lib/action_controller/metal/rendering.rb | 8 | ||||
-rw-r--r-- | actionpack/lib/action_controller/renderer.rb | 103 | ||||
-rw-r--r-- | actionpack/test/controller/renderer_test.rb | 99 |
5 files changed, 216 insertions, 0 deletions
diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index a9e8b82107..d8c26fa281 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,3 +1,8 @@ +* Add `ActionController::Renderer` to render arbitrary templates + outside controller actions. + + *Ravil Bayramgalin* + * Support `:assigns` option when rendering with controllers/mailers. *Ravil Bayramgalin* diff --git a/actionpack/lib/action_controller.rb b/actionpack/lib/action_controller.rb index e977bbce99..7667e469d3 100644 --- a/actionpack/lib/action_controller.rb +++ b/actionpack/lib/action_controller.rb @@ -11,6 +11,7 @@ module ActionController autoload :Caching autoload :Metal autoload :Middleware + autoload :Renderer autoload_under "metal" do autoload :Compatibility diff --git a/actionpack/lib/action_controller/metal/rendering.rb b/actionpack/lib/action_controller/metal/rendering.rb index 7bbff0450a..2370c607ed 100644 --- a/actionpack/lib/action_controller/metal/rendering.rb +++ b/actionpack/lib/action_controller/metal/rendering.rb @@ -4,6 +4,14 @@ module ActionController RENDER_FORMATS_IN_PRIORITY = [:body, :text, :plain, :html] + module ClassMethods + # Returns a renderer class (inherited from ActionController::Renderer) + # for the controller. + def renderer + @renderer ||= Renderer.for(self) + end + end + # Before processing, set the request formats in current controller formats. def process_action(*) #:nodoc: self.formats = request.formats.map(&:ref).compact diff --git a/actionpack/lib/action_controller/renderer.rb b/actionpack/lib/action_controller/renderer.rb new file mode 100644 index 0000000000..a122954968 --- /dev/null +++ b/actionpack/lib/action_controller/renderer.rb @@ -0,0 +1,103 @@ +module ActionController + # ActionController::Renderer allows to render arbitrary templates + # without requirement of being in controller actions. + # + # You get a concrete renderer class by invoking ActionController::Base#renderer. + # For example, + # + # ApplicationController.renderer + # + # It allows you to call method #render directly. + # + # ApplicationController.renderer.render template: '...' + # + # You can use a shortcut on controller to replace previous example with: + # + # ApplicationController.render template: '...' + # + # #render method allows you to use any options as when rendering in controller. + # For example, + # + # FooController.render :action, locals: { ... }, assigns: { ... } + # + # The template will be rendered in a Rack environment which is accessible through + # ActionController::Renderer#env. You can set it up in two ways: + # + # * by changing renderer defaults, like + # + # ApplicationController.renderer.defaults # => hash with default Rack environment + # + # * by initializing an instance of renderer by passing it a custom environment. + # + # ApplicationController.renderer.new(method: 'post', https: true) + # + class Renderer + class_attribute :controller, :defaults + # Rack environment to render templates in. + attr_reader :env + + class << self + delegate :render, to: :new + + # Create a new renderer class for a specific controller class. + def for(controller) + Class.new self do + self.controller = controller + self.defaults = { + http_host: 'example.org', + https: false, + method: 'get', + script_name: '', + 'rack.input' => '' + } + end + end + end + + # Accepts a custom Rack environment to render templates in. + # It will be merged with ActionController::Renderer.defaults + def initialize(env = {}) + @env = normalize_keys(defaults).merge normalize_keys(env) + @env['action_dispatch.routes'] = controller._routes + end + + # Render templates with any options from ActionController::Base#render_to_string. + def render(*args) + raise 'missing controller' unless controller? + + instance = controller.build_with_env(env) + instance.render_to_string(*args) + end + + private + def normalize_keys(env) + env.dup.tap do |new_env| + convert_symbols! new_env + handle_method_key! new_env + handle_https_key! new_env + end + end + + def convert_symbols!(env) + env.keys.each do |key| + if key.is_a? Symbol + value = env.delete key + key = key.to_s.upcase + env[key] = value + end + end + end + + def handle_method_key!(env) + if method = env.delete('METHOD') + env['REQUEST_METHOD'] = method.upcase + end + end + + def handle_https_key!(env) + if env.has_key? 'HTTPS' + env['HTTPS'] = env['HTTPS'] ? 'on' : 'off' + end + end + end +end diff --git a/actionpack/test/controller/renderer_test.rb b/actionpack/test/controller/renderer_test.rb new file mode 100644 index 0000000000..6efc9c8dfd --- /dev/null +++ b/actionpack/test/controller/renderer_test.rb @@ -0,0 +1,99 @@ +require 'abstract_unit' + +class RendererTest < ActiveSupport::TestCase + test 'creating with a controller' do + controller = CommentsController + renderer = ActionController::Renderer.for controller + + assert_equal controller, renderer.controller + end + + test 'creating from a controller' do + controller = AccountsController + renderer = controller.renderer + + assert_equal controller, renderer.controller + end + + test 'rendering with a class renderer' do + renderer = ApplicationController.renderer + content = renderer.render template: 'ruby_template' + + assert_equal 'Hello from Ruby code', content + end + + test 'rendering with an instance renderer' do + renderer = ApplicationController.renderer.new + content = renderer.render file: 'test/hello_world' + + assert_equal 'Hello world!', content + end + + test 'rendering with locals' do + renderer = ApplicationController.renderer + content = renderer.render template: 'test/render_file_with_locals', + locals: { secret: 'bar' } + + assert_equal "The secret is bar\n", content + end + + test 'rendering with assigns' do + renderer = ApplicationController.renderer + content = renderer.render template: 'test/render_file_with_ivar', + assigns: { secret: 'foo' } + + assert_equal "The secret is foo\n", content + end + + test 'rendering with custom env' do + renderer = ApplicationController.renderer.new method: 'post' + content = renderer.render inline: '<%= request.post? %>' + + assert_equal 'true', content + end + + test 'rendering with defaults' do + renderer = ApplicationController.renderer + renderer.defaults[:https] = true + content = renderer.render inline: '<%= request.ssl? %>' + + assert_equal 'true', content + end + + test 'same defaults from the same controller' do + defaults = ->(controller) { controller.renderer.defaults } + + assert defaults[AccountsController].equal? defaults[AccountsController] + assert_not defaults[AccountsController].equal? defaults[CommentsController] + end + + test 'rendering with different formats' do + html = 'Hello world!' + xml = "<p>Hello world!</p>\n" + + assert_equal html, render['respond_to/using_defaults'] + assert_equal xml, render['respond_to/using_defaults.xml.builder'] + assert_equal xml, render['respond_to/using_defaults', formats: :xml] + end + + test 'rendering with helpers' do + assert_equal "<p>1\n<br />2</p>", render[inline: '<%= simple_format "1\n2" %>'] + end + + test 'rendering from inherited renderer' do + inherited = Class.new ApplicationController.renderer do + defaults[:script_name] = 'script' + def render(options) + super options.merge(locals: { param: :value }) + end + end + + template = '<%= url_for controller: :foo, action: :bar, param: param %>' + assert_equal 'script/foo/bar?param=value', inherited.render(inline: template) + end + + private + def render + @render ||= ApplicationController.renderer.method(:render) + end +end |