Skip to content

Views

MVC::Keayl::View renders templates. It resolves a template by name, format, and handler, compiles it through a pluggable handler, and caches the result. HAML is the default handler, backed by Template::HAML.

1
2
3
4
5
use MVC::Keayl::View;

my $view = MVC::Keayl::View.new(:paths(['app/views']));

$view.render-template('users/show', { user => $user });

Template resolution

render-template resolves a name against each search path in turn, then by handler extension, preferring a format-qualified file:

1
2
3
app/views/users/show.html+phone.haml  # users/show, format html, variant phone
app/views/users/show.html.haml        # users/show, format html
app/views/users/show.haml             # format-less fallback

The default format is html, overridable per call with :format. A :variant prefers a variant-qualified file before the plain one, so phone resolves show.html+phone.haml and falls back to show.html.haml. When the View is given a controller, a name with no / is resolved relative to the controller's path (UsersController resolves under users/).

Handlers

A handler compiles a template source and renders it with locals. The default haml handler renders HAML. Register others by extension:

1
$view.register-handler('txt', MyTextHandler.new);

A handler does the MVC::Keayl::View::Handler role: compile($source) returns a compiled template, and render($compiled, %locals) produces the output.

Caching and reloading

Compiled templates are cached by file. reload (on by default) re-checks the file's modification time and recompiles when it changes, which suits development. Setting cache => False disables caching so every render recompiles:

1
my $view = MVC::Keayl::View.new(:paths(['app/views']), :!reload);   # trust the cache

Inline rendering

render-inline renders a template string directly, without resolving a file.

Layouts

When an action renders a template, the rendered body is wrapped in a layout from layouts/. With no layout chosen, the application layout is used if it exists; otherwise the template renders on its own. Inside the layout, the wrapped body is available through the yield helper:

1
2
3
%html
  %body
    != yield()

Choose a layout per action with the :layout option, or for every action of a controller with the class-level layout declaration:

1
2
3
4
5
6
self.render('show', :layout('print'));   # this action only

class AdminController is MVC::Keayl::Controller { }
AdminController.layout('admin');          # every action

self.render('show', :layout(False));      # no layout

The layout declaration also has an is layout trait form for the class header:

1
class AdminController is MVC::Keayl::Controller is layout('admin') { }

An action-level :layout overrides a controller declaration, which overrides the application default.

content-for and yield

A template captures named content with content-for, and the layout places it with yield:

1
2
3
4
5
6
7
-# in the template
= content-for('title', 'Dashboard')

-# in the layout
%title= yield('title')
%body
  != yield()

yield() returns the main body; yield(name) returns the matching content-for capture, or an empty string when none was set.

Partials

A partial is a template whose file name begins with an underscore. It is referred to without the underscore. render-partial renders one with locals:

1
$view.render-partial('users/form', { user => $user });   # renders users/_form

A name with no path segment resolves under the controller's view path; a name with a / resolves from the views root, so shared/menu renders shared/_menu from any controller.

Inside a template, the partial helper embeds a partial. Use != so its HTML is not escaped:

1
2
%ul
  != partial('users/row', %( user => $user ))

Object partials

render-object derives the partial from an object. By default the partial name and the local both come from the object's class name; an object can override the path with a to-partial-path method:

1
2
3
4
5
class Post {
  method to-partial-path { 'posts/post' }   # renders posts/_post, local `post`
}

$view.render-object($post);                  # in a template: != partial-for($post)

A controller renders an object directly with render($post).

Collection partials

render-collection renders a partial once per item. Each render receives the item under the partial's local name and a zero-based {local}_counter:

1
2
-# greetings/_line.html.haml
%p= $line ~ ' #' ~ $line_counter
1
!= partial-each('greetings/line', $lines)

A spacer partial renders between items:

1
$view.render-collection('greetings/line', @lines, spacer => 'greetings/divider');

A controller renders a collection with render(:partial('line'), :collection(@lines)).

Output safety

HAML escapes interpolated values by default, so = $value is safe and != $value emits raw HTML. MVC::Keayl::SafeString adds a safe-buffer type and helpers for composing and cleaning HTML.

html-escape encodes the markup-significant characters. html-safe and raw wrap a string as a SafeString, which reports is-html-safe and stringifies to its raw content. Concatenation escapes anything that is not already safe:

1
2
html-safe('<b>a</b>').concat('<unsafe>');     # <b>a</b>&lt;unsafe&gt;
safe-join([html-safe('<b>a</b>'), '<x>']);    # <b>a</b>&lt;x&gt;

sanitize keeps an allowlist of tags and attributes, removes script and style elements with their content, and strips event-handler attributes and javascript: URLs:

1
2
sanitize('<a href="javascript:x()" onclick="y()">hi</a>');   # <a>hi</a>
sanitize('<b>ok</b>', tags => <b i em>);                     # custom allowlist

json-escape encodes the characters that could end a <script> block (<, >, &, and the line and paragraph separators) as \uXXXX, for embedding JSON in a script context.

In templates these are available as the escape, raw, sanitize, and json helpers; pair sanitize/raw with != so the cleaned HTML is not escaped again.