The render plugin adds support for template rendering using the tilt library. Two methods are provided for template rendering, view (which uses the layout) and render (which does not).
plugin :render route do |r| r.is 'foo' do view('foo') # renders views/foo.erb inside views/layout.erb end r.is 'bar' do render('bar') # renders views/bar.erb end end
The render and view methods just return strings, they do not have side effects (unless the templates themselves have side effects). As Roda uses the routing block return value as the body of the response, in most cases you will call these methods as the last expression in a routing block to have the response body be the result of the template rendering.
Because render and view just return strings, you can call them inside templates (i.e. for subtemplates/partials), or multiple times in the same route and combine the results together:
route do |r| r.is 'foo-bars' do @bars = Bar.where(:foo).map{|b| render(:bar, locals: {bar: b})}.join view('foo') end end
You can provide options to the plugin method:
plugin :render, engine: 'haml', views: 'admin_views'
Plugin Options
The following plugin options are supported:
| :allowed_paths |
Set the template directories to allow. Attempts to render paths outside of these directories will raise an error. Defaults to the |
| :assume_fixed_locals |
Set if you are sure all templates in your application use fixed locals to allow for additional optimization. This is ignored unless both compiled methods and fixed locals are not supported. |
| :cache |
nil/false to explicitly disable permanent template caching. By default, permanent template caching is disabled by default if RACK_ENV is development. When permanent template caching is disabled, for templates with paths in the file system, the modification time of the file will be checked on every render, and if it has changed, a new template will be created for the current content of the file. |
| :cache_class |
A class to use as the template cache instead of the default. |
| :check_paths |
Can be set to false to turn off template path checking. |
| :engine |
The tilt engine to use for rendering, also the default file extension for templates, defaults to ‘erb’. |
| :escape |
Use Erubi as the ERB template engine, and enable escaping by default, which makes |
| :layout |
The base name of the layout file, defaults to ‘layout’. This can be provided as a hash with the :template or :inline options. |
| :layout_opts |
The options to use when rendering the layout, if different from the default options. |
| :template_opts |
The tilt options used when rendering all templates. defaults to: |
| :engine_opts |
The tilt options to use per template engine. Keys are engine strings, values are hashes of template options. |
| :views |
The directory holding the view files, defaults to the ‘views’ subdirectory of the application’s :root option (the process’s working directory by default). |
Render/View Method Options
Most of these options can be overridden at runtime by passing options to the view or render methods:
view('foo', engine: 'html.erb') render('foo', views: 'admin_views')
There are additional options to view and render that are available at runtime:
| :cache |
Set to false to not cache this template, even when caching is on by default. Set to true to force caching for this template, even when the default is to not permantently cache (e.g. when using the :template_block option). |
| :cache_key |
Explicitly set the hash key to use when caching. |
| :content |
Only respected by |
| :inline |
Use the value given as the template code, instead of looking for template code in a file. |
| :locals |
Hash of local variables to make available inside the template. |
| :path |
Use the value given as the full pathname for the file, instead of using the :views and :engine option in combination with the template name. |
| :scope |
The object in which context to evaluate the template. By default, this is the |
| :template |
Provides the name of the template to use. This allows you pass a single options hash to the render/view method, while still allowing you to specify the template name. |
| :template_block |
|
| :template_class |
Provides the template class to use, instead of using Tilt or |
Here’s an example of using these options:
view(inline: '<%= @foo %>') render(path: '/path/to/template.erb')
If you pass a hash as the first argument to view or render, it should have either :template, :inline, :path, or :content (for view) as one of the keys.
Fixed Locals in Templates
By default, you can pass any local variables to any templates. A separate template method is compiled for each combination of locals. This causes multiple issues:
-
It is inefficient, especially for large templates that are called with many combinations of locals.
-
It hides issues if unused local variable names are passed to the template
-
It does not support default values for local variables
-
It does not support required local variables
-
It does not support cases where you want to pass values via a keyword splat
-
It does not support named blocks
If you are using Tilt 2.6+, you can used fixed locals in templates, by passing the appropriate options in :template_opts. For example, if you are using ERB templates, the recommended way to use the render plugin is to use the :extract_fixed_locals and :default_fixed_locals template options:
plugin :render, template_opts: {extract_fixed_locals: true, default_fixed_locals: '()'}
This will default templates to not allowing any local variables to be passed. If the template requires local variables, you can specify them using a magic comment in the template, such as:
<%# locals(required_local:, optional_local: nil) %>
The magic comment is used as method parameters when defining the compiled template method.
For better debugging of issues with invalid keywords being passed to templates that have not been updated to support fixed locals, it can be helpful to set :default_fixed_locals to use a single optional keyword argument '(_no_kw: nil)'. This makes the error message show which keywords were passed, instead of showing that the takes no arguments (if you use '()'), or that no keywords are accepted (if you pass (**nil)).
If you are sure your application works with all templates using fixed locals, set the :assume_fixed_locals render plugin option, which will allow the plugin to optimize cache lookup for renders with locals, and avoid duplicate compiled methods for templates rendered both with and without locals.
See Tilt’s documentation for more information regarding fixed locals.
Speeding Up Template Rendering
The render/view method calls are optimized for usage with a single symbol/string argument specifying the template name. So for fastest rendering, pass only a symbol/string to render/view. Next best optimized are template calls with a single :locals option. Use of other options disables the compiled template method optimizations and can be significantly slower.
If you must pass a hash to render/view, either as a second argument or as the only argument, you can speed things up by specifying a :cache_key option in the hash, making sure the :cache_key is unique to the template you are rendering.
Recommended template_opts
Here are the recommended values of :template_opts for new applications (a couple are Erubi-specific and can be ignored if you are using other templates engines):
plugin :render, assume_fixed_locals: true, # Optimize plugin by assuming all templates use fixed locals template_opts: { scope_class: self, # Always uses current class as scope class for compiled templates freeze: true, # Freeze string literals in templates extract_fixed_locals: true, # Support fixed locals in templates default_fixed_locals: '()', # Default to templates not supporting local variables escape: true, # For Erubi templates, escapes <%= by default (use <%== for unescaped chain_appends: true, # For Erubi templates, improves performance skip_compiled_encoding_detection: true, # Unless you need encodings explicitly specified }
Accepting Template Blocks in Methods
If you are used to Rails, you may be surprised that this type of template code doesn’t work in Roda:
<%= some_method do %> Some HTML <% end %>
The reason this doesn’t work is that this is not valid ERB syntax, it is Rails syntax, and requires attempting to parse the some_method do Ruby code with a regular expression. Since Roda uses ERB syntax, it does not support this.
In general, these methods are used to wrap the content of the block and inject the content into the output. To get similar behavior with Roda, you have a few different options you can use.
Use Erubi::CaptureBlockEngine
Roda defaults to using Erubi for erb template rendering. Erubi 1.13.0+ includes support for an erb variant that supports blocks in <%= and <%== tags. To use it:
require 'erubi/capture_block' plugin :render, template_opts: {engine_class: Erubi::CaptureBlockEngine}
See the Erubi documentation for how to capture data inside the block. Make sure the method call (some_method in the example) returns the output you want added to the rendered body.
Directly Inject Template Output
You can switch from a <%= tag to using a <% tag:
<% some_method do %> Some HTML <% end %>
While this would output Some HTML into the template, it would not be able to inject content before or after the block. However, you can use the inject_erb_plugin to handle the injection:
def some_method inject_erb "content before block" yield inject_erb "content after block" end
If you need to modify the captured block before injecting it, you can use the capture_erb plugin to capture content from the template block, and modify that content, then use inject_erb to inject it into the template output:
def some_method(&block) inject_erb "content before block" inject_erb capture_erb(&block).upcase inject_erb "content after block" end
This is the recommended approach for handling this type of method, if you want to keep the template block in the same template.
Separate Block Output Into Separate Template
By moving the Some HTML into a separate template, you can render that template inside the block:
<%= some_method{render('template_name')} %>
It’s also possible to use an inline template:
<%= some_method do render(:inline=><<-END) Some HTML END end %>
This approach is useful if it makes sense to separate the template block into its own template. You lose the ability to use local variable from outside the template block inside the template block with this approach.
Separate Header and Footer
You can define two separate methods, one that outputs the content before the block, and one that outputs the content after the block, and use those instead of a single call:
<%= some_method_before %> Some HTML <%= some_method_after %>
This is the simplest option to setup, but it is fairly tedious to use.
Classes and Modules
Constants
| COMPILED_METHOD_SUPPORT | = | RUBY_VERSION >= '2.3' && tilt_compiled_method_support && ENV['RODA_RENDER_COMPILED_METHOD_SUPPORT'] != 'no' | ||
| FIXED_LOCALS_COMPILED_METHOD_SUPPORT | = | COMPILED_METHOD_SUPPORT && Tilt::Template.method_defined?(:fixed_locals?) | ||
| NO_CACHE | = | {:cache=>false}.freeze |
Public Class methods
Setup default rendering options. See Render for details.
# File lib/roda/plugins/render.rb 325 def self.configure(app, opts=OPTS) 326 if app.opts[:render] 327 orig_cache = app.opts[:render][:cache] 328 orig_method_cache = app.opts[:render][:template_method_cache] 329 opts = app.opts[:render][:orig_opts].merge(opts) 330 end 331 app.opts[:render] = opts.dup 332 app.opts[:render][:orig_opts] = opts 333 334 opts = app.opts[:render] 335 opts[:engine] = (opts[:engine] || "erb").dup.freeze 336 opts[:views] = app.expand_path(opts[:views]||"views").freeze 337 opts[:check_paths] = true unless opts.has_key?(:check_paths) 338 opts[:allowed_paths] ||= [opts[:views]] 339 opts[:allowed_paths] = opts[:allowed_paths].map do |f| 340 path = app.expand_path(f, nil) 341 if File.directory?(path) || f == opts[:views] 342 path << "/" 343 elsif opts[:check_paths] 344 # RODA4: raise 345 RodaPlugins.warn("allowed path #{f.inspect} is not a directory in the file system. For backwards compatibility, will allow prefix matches, but this could result in security issues. Roda 4 will raise if an allowed path is not a directory.") 346 end 347 path.freeze 348 end.uniq.freeze 349 opts[:assume_fixed_locals] &&= FIXED_LOCALS_COMPILED_METHOD_SUPPORT 350 351 unless opts.has_key?(:check_template_mtime) 352 opts[:check_template_mtime] = if opts[:cache] == false || opts[:explicit_cache] 353 true 354 else 355 ENV['RACK_ENV'] == 'development' 356 end 357 end 358 359 begin 360 app.const_get(:RodaCompiledTemplates, false) 361 rescue NameError 362 compiled_templates_module = Module.new 363 app.send(:include, compiled_templates_module) 364 app.const_set(:RodaCompiledTemplates, compiled_templates_module) 365 end 366 opts[:template_method_cache] = orig_method_cache || (opts[:cache_class] || RodaCache).new 367 opts[:template_method_cache][:_roda_layout] = nil if opts[:template_method_cache][:_roda_layout] 368 opts[:cache] = orig_cache || (opts[:cache_class] || RodaCache).new 369 370 opts[:layout_opts] = (opts[:layout_opts] || {}).dup 371 opts[:layout_opts][:_is_layout] = true 372 if opts[:layout_opts][:views] 373 opts[:layout_opts][:views] = app.expand_path(opts[:layout_opts][:views]).freeze 374 end 375 376 if layout = opts.fetch(:layout, true) 377 opts[:layout] = true 378 379 case layout 380 when Hash 381 opts[:layout_opts].merge!(layout) 382 when true 383 opts[:layout_opts][:template] ||= 'layout' 384 else 385 opts[:layout_opts][:template] = layout 386 end 387 388 opts[:optimize_layout] = (opts[:layout_opts][:template] if opts[:layout_opts].keys.sort == [:_is_layout, :template]) 389 end 390 opts[:layout_opts].freeze 391 392 template_opts = opts[:template_opts] = (opts[:template_opts] || {}).dup 393 template_opts[:outvar] ||= '@_out_buf' 394 unless template_opts.has_key?(:default_encoding) 395 template_opts[:default_encoding] = Encoding.default_external 396 end 397 398 engine_opts = opts[:engine_opts] = (opts[:engine_opts] || {}).dup 399 engine_opts.to_a.each do |k,v| 400 engine_opts[k] = v.dup.freeze 401 end 402 403 if escape = opts[:escape] 404 require 'tilt/erubi' 405 406 case escape 407 when String, Array 408 Array(escape).each do |engine| 409 engine_opts[engine] = (engine_opts[engine] || {}).merge(:escape => true).freeze 410 end 411 else 412 template_opts[:escape] = true 413 end 414 end 415 416 template_opts.freeze 417 engine_opts.freeze 418 opts.freeze 419 end
# File lib/roda/plugins/render.rb 313 def self.tilt_template_compiled_method(template, locals_keys, scope_class) 314 template.send(:compiled_method, locals_keys, scope_class) 315 end
# File lib/roda/plugins/render.rb 301 def self.tilt_template_fixed_locals?(template) 302 template.fixed_locals? 303 end