Helpers that build HTML.
View helpers return SafeString values, so their markup is not re-escaped when emitted
with !=. In templates they are bare calls routed through the view context
(link-to, image-tag, and so on), with arguments and no sigil. Locals keep
their sigil, so $user reads as a value and link-to(...) reads as a call.
#tag building
content-tag builds an element with content and tag a self-closing one.
Attribute values are escaped, a True value becomes a bare boolean attribute, and a
False or undefined value is dropped. A data or aria hash
expands to dasherized attributes, with structured values JSON encoded.
# <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' ),
));
A class attribute accepts an array of tokens or a hash of conditional tokens, and
class-names builds the same token list on its own, dropping falsy and duplicate
tokens.
# class="a b"
tag('span', %( class => ['a', 'b'] ));
# class="active" when $on
content-tag('div', 'x', %(
class => %( active => $on ),
));
# "btn active"
class-names('btn', %(
active => True,
disabled => False,
));
javascript-tag wraps script content in a <script> element,
time-tag builds a <time> with a datetime attribute, and
auto-discovery-link-tag builds a feed link. atom-feed assembles a full
Atom document from a feed builder.
# <script>alert(1)</script>
javascript-tag('alert(1)');
# <time datetime="2020-01-01">New Year</time>
time-tag(Date.new(2020, 1, 1), 'New Year');
auto-discovery-link-tag('atom', '/feed.atom');
#url helpers
url-for turns a target into a URL string, passing a string through and reading
path from a hash. link-to builds an anchor, and button-to
builds a small form that posts to a URL, adding a _method override for other verbs.
# <a class="nav" href="/posts">Posts</a>
link-to('Posts', '/posts', %( class => 'nav' ));
# <a href="/only">/only</a>
link-to('/only');
button-to('Delete', '/posts/1', %(
method => 'delete',
));
#asset helpers
asset-path resolves a source to a URL: a bare source is prefixed with
/assets/, an absolute path or external URL is left alone, and a :type
adds an extension when the source has none. image-tag,
stylesheet-link-tag, and javascript-include-tag build their elements,
resolving sources through asset-path.
# /assets/logo.png
asset-path('logo.png');
# /assets/app.css
asset-path('app', :type('css'));
# <img alt="Logo" src="/assets/logo.png" />
image-tag('logo.png');
stylesheet-link-tag('app', 'admin');
javascript-include-tag('app');
The asset helpers accept a resolver, a routine of ($source, $type) that
returns the final URL. It is the seam for fingerprinting or a CDN host, and a
MVC::Keayl::View carries an asset-resolver its helpers use.
my $view = MVC::Keayl::View.new(
:paths(['app/views']),
asset-resolver => -> $source, $type {
'https://cdn.example/' ~ $source
},
);
#forms
form-with builds a form around a FormBuilder. The builder is passed to a
content callback and scopes field names to the model (or an explicit
scope), prefills values, and annotates errors. The form posts by default, adding a
_method override for other verbs and an authenticity_token field when a
csrf-token is given.
form-with(
model => $post,
url => '/posts/1',
method => 'patch',
csrf-token => $token,
content => -> $f {
$f.text-field('title') ~ $f.submit('Save')
},
);
The builder provides text-field, password-field,
hidden-field, text-area, check-box,
radio-button, select, label, submit, and
button, plus the HTML5 typed inputs (email-field,
number-field, date-field, and the rest). Names are scoped
(post[title]) and ids derived (post_title). A check-box
renders a hidden companion, a password-field never emits a value, and
file-field takes multiple and direct-upload options.
# type="email"
$f.email-field('email');
# type="date"
$f.date-field('born');
# name="post[photos][]"
$f.file-field('photos', %( multiple => True ));
$f.fields-for('author', block => -> $author {
# name="post[author][name]"
$author.text-field('name')
});
When the builder has a model, fields prefill from it by calling the attribute-named method, an
explicit value overrides the model, and a field whose attribute reports errors gets a
field-with-errors class. The builder reads errors through the model's
errors-on($attribute) method, and errors-for renders the messages.
Outside a builder, button-tag and image-submit-tag build a submit button
and an image submit input.
#select and option helpers
options-for-select builds <option> tags from strings, label-value
pairs, or [label, value, attrs] triples, marking the selected value.
options-from-collection-for-select reads value and text methods off a collection, and
grouped-options-for-select wraps choices in <optgroup> elements.
select-tag wraps option markup in a <select>.
# <option value="a">a</option><option selected value="b">b</option>
options-for-select(['a', 'b'], 'b');
options-from-collection-for-select(
@cities,
'id',
'name',
$selected,
);
select-tag(
'city',
options-for-select(['NY', 'LA']),
%( include-blank => True ),
);
The date-part helpers select-year, select-month (with month names),
select-day, select-hour, select-minute, and
select-second build one select each, and select-date and
select-time combine them. On the builder, collection-select,
collection-radio-buttons, and collection-check-boxes build controls from
a collection, and date-select, time-select, and
datetime-select build multiparameter selects prefilled from a model.
# year, month, and day selects
select-date(Date.new(2020, 3, 15));
$f.collection-select(
'city-id',
@cities,
'id',
'name',
);
# post[born](1i), (2i), (3i)
$f.date-select('born');
#simple_form-style inputs
SimpleFormBuilder adds an input method that assembles a label, control,
hint, and error into a wrapper, inferring the control type. simple-form-for wraps it
in a form like form-with. The type comes from as, then the attribute name
(password, email, the long-text names), then a boolean model value,
defaulting to a string input. A required attribute marks the wrapper and stars the label.
simple-form-for(
$post,
url => '/posts',
required => ['title'],
content => -> $f {
$f.input('title', hint => 'Keep it short')
# textarea, inferred
~ $f.input('body')
# checkbox, inferred from a boolean
~ $f.input('published')
~ $f.input('state',
as => 'select',
collection => ['draft', 'live'])
});
#formatting helpers
text
truncate cuts a string to a length and can break at a separator,
pluralize pairs a count with a singular or pluralized word, and
simple-format wraps text in paragraphs. highlight wraps phrase matches,
excerpt extracts a window around a phrase, and word-wrap breaks lines at
a width. strip-tags, strip-links, and sanitize-css clean
untrusted markup.
# This is a...
truncate('This is a long sentence', length => 12);
# 2 people
pluralize(2, 'person');
# <p>Para one</p>\n<p>Para two</p>
simple-format("Para one\n\nPara two");
# You searched for <mark>tiger</mark>
highlight('You searched for tiger', 'tiger');
# Visit here now
strip-links('Visit <a href="/x">here</a> now');
cycle rotates through values across calls, capture returns a block's
output as a safe string, and provide accumulates content under a name for a layout
yield.
numbers
# 1,234,567
number-with-delimiter(1234567);
# $1,234.50
number-to-currency(1234.5);
# 66.7%
number-to-percentage(66.666, precision => 1);
# 1.5 KB
number-to-human-size(1536);
# (123) 555-1234
number-to-phone(1235551234, area-code => True);
# 12.3 Million
number-to-human(12345678);
dates and times
distance-of-time-in-words describes the gap between two times in words, with an
optional include-seconds for finer detail under a minute.
time-ago-in-words is the same measured against the current time.
# about 5 hours
distance-of-time-in-words($from, $to);
# less than 5 seconds
distance-of-time-in-words(
$from,
$to,
include-seconds => True,
);
# 3 days ago
time-ago-in-words($posted-at);
#helper modules
Beyond the built-in helpers, an application defines its own as our subs in modules
under app/helpers/. Each sub becomes a bare template call.
ApplicationHelper is global, available in every view.
-# app/helpers/ApplicationHelper.rakumod
unit module ApplicationHelper;
our sub nav-link($label, $href) {
qq{<a href="$href">$label</a>}
}
%nav
!= nav-link('Home', '/')
A per-controller helper named after the controller (UsersController →
app/helpers/UsersHelper.rakumod) is available only in that controller's views, and the
chain follows controller inheritance: a controller sees its own helper, its ancestors' helpers, and
ApplicationHelper. A helper is a plain function. For request state, read
$*KEAYL-CONTROLLER.
our sub current-user-name {
$*KEAYL-CONTROLLER.current-user.name
}
Helper modules reload on change in development, the same as templates. The controller
and scaffold generators write a matching helper module, and keayl new
writes ApplicationHelper.