Skip to content

Render context

HAML.render accepts a :context named argument — a Raku object whose methods become reachable inside templates as bare identifiers in expression position. This is the "view context" of frameworks like Rails: a place to put template-facing helpers without polluting global scope.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class MyView {
  has Str $.title;
  method greeting { 'world' }
}

HAML.render(
  :src(q:to/HAML/),
    %h1= title
    %p= greeting
    HAML
  :context(MyView.new(:title('Hello'))),
);

renders:

1
2
<h1>Hello</h1>
<p>world</p>

Bare identifiers

A "bare identifier" is an expression that is exactly one name — letters, digits, underscore, hyphen — with no parentheses, sigils, or operators. Examples:

Expression Bare identifier?
title yes
user-count yes
$title no — has sigil
title() no — has parens
title.uc no — has .

A bare identifier in =, !=, &=, ~, or in a control-flow condition (if, unless, elsif, while, repeat, given) resolves through:

  1. Locals — if %locals{name} exists, use it.
  2. Context method — otherwise if context.^can(name) is true, call context.name.
  3. Fallback to eval — otherwise the expression is handed to the normal Raku evaluator. If the name isn't a valid Raku identifier in scope, you'll get X::HAML::Eval.

Context methods with arguments

A context method is also callable as a bare name with arguments, so a helper that takes parameters reads like a plain function call, no sigil:

1
2
3
class AppView does Template::HAML::HelpersRole {
  method link-to($text, $href) { qq{<a href="$href">$text</a>} }
}
1
2
3
%nav
  != link-to('Home', '/')
  != link-to(label, url)

Each context method named in an expression is bound as a lexical routine for that render, dispatching to the context. Locals are still referenced with their sigil ($label), since only standalone bare identifiers resolve to a local. Built-in helpers (surround, list-of, …) remain callable as before. This works in the interpreted renderer and the default direct-emit renderer.

1
2
3
4
- if is-admin
  %p= admin-greeting
- else
  %p= public-greeting

Overriding a built-in helper

A context method takes precedence over the built-in helper of the same name. To replace surround, partial, list-of, or any other built-in for a given render, define a method with that name on your context:

1
2
3
class AppView does Template::HAML::HelpersRole {
  method surround($pre, $post, &block) { "{$pre}OVERRIDDEN{$post}" }
}
1
2
%div
  != surround('[', ']', { 'inner' })

renders <div>[OVERRIDDEN]</div> instead of the built-in [inner]. Because the default ViewContext exposes the built-ins as methods, every built-in name is resolved through the context, so overriding one is just defining it.

Declaring helpers explicitly

Helper discovery normally introspects the context with .^methods. Helpers installed in a way that introspection does not report, such as methods added at runtime with ^add_method or handled through FALLBACK, are invisible to that scan. Declare them with a haml-helper-names method that returns their names:

1
2
3
4
class DynamicView does Template::HAML::HelpersRole {
  method haml-helper-names { <ping pong> }
  method FALLBACK($name, |args) { ... }
}

When haml-helper-names is present its list is trusted verbatim: each entry is kept if it is a valid helper name (starts with a lowercase letter or underscore, then word characters or hyphens), then de-duplicated and sorted. The list is not cached, so a context whose helper set varies per instance can return a different list each time.

Default context

If you don't pass :context, the renderer constructs a Template::HAML::ViewContext for you. It composes the Template::HAML::HelpersRole role, so every built-in helper (surround, escape-once, list-of, find-and-preserve, html-safe, capture-haml, yield, content-for, tab-up, tab-down, haml-concat) is reachable as a method on the context.

Extending the context

Two composition points are supported.

Mix in the helper role into your own class:

1
2
3
4
5
6
7
8
use Template::HAML::HelpersRole;

class AppContext does Template::HAML::HelpersRole {
  has $.user;
  method current-user-name { $!user.name }
}

HAML.render(:src($src), :context(AppContext.new(:$user)));

The role brings the built-in helpers along, so you can add your own methods without losing surround, list-of, etc.

Subclass ViewContext:

1
2
3
4
5
use Template::HAML::ViewContext;

class AppContext is Template::HAML::ViewContext {
  method site-name { 'MySite' }
}

Either approach works; the role form is slightly more flexible because you can compose other roles alongside it:

1
2
3
4
role Greeter { method hello   { 'hi'     } }
role Footer  { method footer  { 'fineprint' } }

class AppContext does Template::HAML::HelpersRole does Greeter does Footer { }

Bare identifiers (hello, footer, surround, …) resolve against every composed role.

Locals still win

When both a local and a context method have the same name, the local wins:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class MyView {
  method title { 'from-context' }
}

HAML.render(
  :src("%h1= title\n"),
  :context(MyView.new),
  :locals(%(title => 'from-locals')),
);
# → <h1>from-locals</h1>

This is the same precedence used by Rails-style view contexts and keeps per-render values from being silently shadowed by a stray helper method.