#template resolution

render-template resolves a name against each search path in turn, then by handler extension, preferring a format-qualified file. The default format is html, and a :variant prefers a variant-qualified file before the plain one. A name with no / resolves relative to the controller's path.

# users/show, format html, variant phone
app/views/users/show.html+phone.haml
# users/show, format html
app/views/users/show.html.haml
# format-less fallback
app/views/users/show.haml
use MVC::Keayl::View;

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

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

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; :!reload trusts the cache and cache => False recompiles every render.

#layouts, content-for, and yield

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. Inside the layout, the wrapped body is available through yield, and a template captures named content with content-for that the layout places with yield(name).

-# app/views/layouts/application.html.haml
%html
  %head
    %title= yield('title')
  %body
    != yield()

-# app/views/articles/show.html.haml
= content-for('title', 'Dashboard')
%h1= $article.title

Choose a layout per action with :layout, or for every action with the class-level layout declaration. An action-level :layout overrides the controller declaration, which overrides the application default.

#partials

A partial is a template whose file name begins with an underscore, referred to without it. 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 one; use != so its HTML is not escaped.

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

-# inside a template
%ul
  != partial('users/row', %( user => $user ))

#collection and object partials

render-collection renders a partial once per item, each render receiving the item under the partial's local name and a zero-based {local}_counter. render-object derives the partial from an object's class name, overridable with a to-partial-path method. A controller renders these directly with render.

-# greetings/_line.html.haml
%p= $line ~ ' #' ~ $line_counter

-# in a template
!= partial-each('greetings/line', $lines)

-# with a spacer partial between items
$view.render-collection('greetings/line', @lines,
  spacer => 'greetings/divider');

-# a controller, directly
self.render(:partial('line'),
  :collection(@lines));

#view helpers

View helpers build HTML and return SafeString values, so their markup is not re-escaped when emitted with !=. content-tag builds an element with content and tag a self-closing one; a class attribute accepts an array of tokens or a hash of conditional ones, and nested data / aria hashes expand to dasherized attributes.

# <p class="lead">hi</p>
content-tag('p', 'hi', %( class => 'lead' ));

# <input disabled type="text" />
tag('input', %(
  type => 'text',
  disabled => True ));

# <div data-toggle="modal" data-user-id="5">x</div>
content-tag('div', 'x', %( data => %(
  user_id => 5,
  toggle => 'modal' ) ));

# class="active" when $on
content-tag('div', 'x',
  %( class => %( active => $on ) ));

# "btn active"
class-names('btn', %(
  active => True,
  disabled => False ));

# <time datetime="2020-01-01">New Year</time>
time-tag(Date.new(2020, 1, 1), 'New Year');

In templates the helpers are bare calls routed through the view context: link-to, image-tag, content-tag, and so on, with no sigil. Locals keep their sigil ($user), so a helper reads as a plain function call. URL, asset, date and time, form, number, options, tag, and text helpers ship built in, and an application adds its own. See the helpers page for the full set.

#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. Concatenation escapes anything not already safe, and sanitize keeps an allowlist while stripping scripts and event handlers.

# <b>a</b><unsafe>
html-safe('<b>a</b>').concat('<unsafe>');

# <b>a</b><x>
safe-join([html-safe('<b>a</b>'), '<x>']);

# <a>hi</a>
sanitize(
  '<a href="javascript:x()" onclick="y()">hi</a>');

# custom allowlist
sanitize('<b>ok</b>', tags => <b i em>);

# \uXXXX-encode characters that could end a <script>
json-escape($payload);

In templates these are the escape, raw, sanitize, and json helpers. Pair sanitize or raw with != so the cleaned HTML is not escaped again.