Latte 3.1: A Revolution in Attributes That Will Make Your Life Easier

- David Grudl
- Nette Core | 8292
The new version of Latte 3.1 brings a set of improvements focused on one of the most common tasks in templates – outputting HTML attributes. It might sound like a small thing, but in reality, it's a fundamental shift in the convenience (DX) and security of your code.
The main motivation for these changes came from four key points:
- Greater convenience and flexibility: To be able to write
<div foo="{$val}">and have Latte handle it intelligently - Control over attribute existence: So that the value decides not only the content but also whether the attribute is rendered at all
- Type checking: So that Latte warns you when you try to
output nonsense (e.g., an array into
title) - Backward compatibility: So that changes are evolutionary, not destructive
Before we dive into the details, let's address the most important question you're probably thinking about right now.
Will this break my templates?
No, it won't.
Although Latte 3.1 changes the rendering logic, it was designed with maximum
consideration for backward compatibility. Most common notations still behave the
same way. The changes primarily affect situations that were previously a “gray
zone” (for example, outputting null or a boolean directly into an
attribute), or situations that previously generated nonsense in the HTML.
Let's take a look at what Latte can do now.
Boolean attributes: The end of complex conditions
HTML has a specific category of attributes, such as checked,
disabled, hidden, readonly, or
required. For these, it's not the value that matters, but their
presence.
Previously, you had to use n:attr or {if}...{/if}
conditions. In Latte 3.1, you just send true or false
directly into the attribute:
{* Latte 3.1 *}
<input hidden={=true} readonly={=false}>
{* Result: <input hidden> *}
If the value is truthy, the attribute is rendered. If falsey, the attribute disappears. It's intuitive and the code is much more readable.
If you're using some JS library that requires the presence/absence of an
attribute and you want to achieve this behavior for other attributes as well
(including data-attributes, for example), use the |toggle
filter:
<div uk-grid="{$isGrid|toggle}">
{* If truthy: <div uk-grid> *}
{* If falsey: <div> *}
The value null removes attributes
This is one of the most pleasant changes. Until now, if a variable was
null, it was output as an empty string "". This often
led to empty attributes in HTML (class="", title=""),
which just took up space.
In Latte 3.1, a new universal rule applies: The value
null means the attribute doesn't exist.
<div title="{$title}">
{* If $title === null, it renders: <div> *}
{* Previously it would render: <div title=""> *}
Thanks to this, you don't need to wrap attributes in conditions. You just send the data there, and if there's no data, the attribute won't be rendered.
What if you want to use a filter? For example, in
title={$title|upper}, the upper converts the variable
to a string, so an empty string would be rendered. The solution is to use
null-safe filters, which are not called if the value is null. They are written
with ?| instead of |, so for example
title={$title?|upper}.
Arrays in class
The n:class attribute is loved for its ability to compose
classes from arrays and conditions. Now this superpower is also gained by the
class attribute itself and other attributes expecting
space-separated content.
You can pass an array (including associative with conditions) to the
class="..." attribute, exactly as you're used to:
<div class={[
'btn',
'btn-primary',
'active' => $isActive,
'disabled' => $isDisabled
]}>
If $isActive is true and $isDisabled is false, it
will render <div class="btn btn-primary active">.
The style attribute
The style attribute gains similar flexibility. Instead of
concatenating strings, you can pass an array with values.
<div style={[
background: 'lightblue',
font-size: '16px',
display: $isVisible ? 'block' : 'none'
]}>
{* Renders: <div style="background-color: lightblue; font-size: 16px; display: block"> *}
Data attributes (JSON serialization)
Often we need to pass configuration to JavaScript in HTML. Previously, this
was solved with json_encode. Now you just pass an array or
object:
<div data-config={[ theme: 'dark', version: 2 ]}>
{* Renders: <div data-config='{"theme":"dark","version":2}'> *}
At the same time, true and false are not output
here as "1"/"", but as the strings "true"
and "false" (i.e., again as JSON).
Aria attributes (accessibility)
The WAI-ARIA specification requires textual "true" and
"false" for boolean values. Latte knows this and handles it
for you:
<button aria-expanded="{=true}" aria-checked="{=false}">
{* Renders: <button aria-expanded="true" aria-checked="false"> *}
Enhanced n:attr
If you use n:attr, all these new features will work
here too.
Type checking
As we mentioned at the beginning, the goal is safety and code cleanliness. Therefore, Latte 3.1 adds type checking.
In previous versions of Latte, it occasionally “turned a blind eye”. If
you accidentally sent an array to the title attribute, it output
title="Array". If you sent true, it output
"1". This is practically never what you want.
In Latte 3.1:
- Regular attributes (href, src, id…): Expect stringable or null. If they receive an array, object, or boolean, they generate a warning and the attribute won't be output.
- Boolean attributes (checked…): Expect anything.
- Special (class, style, data-, aria-): Have their own rules described above.
Migration and compatibility
Because the behavior of null changes and in the case of
data- attributes also true/false, Latte offers a way
to help you discover places where the output will change. You can enable
migration warnings:
$latte->setMigrationWarnings();
When this is enabled, Latte compares during rendering how the attribute would look in version 3.0 and how it looks in version 3.1. If the output differs, it throws a warning.
Typically, you'll see warnings in these cases:
- You have
<div class="{$var}">and$varisnull. (Previouslyclass="", now the attribute disappears). - You have
<div data-foo="{$bool}">. (Previously"1"/"", now"true"/"false").
How to resolve found differences?
A) I want the original behavior (forcing a string)
If you want null to be output as an empty attribute,
just use:
<div foo={$foo ?? ''}>
{* Renders null as foo="" *}
Or if you really need the boolean to be output as
"1"/"", just cast the value to a string. This tells
Latte “I know what I'm doing, I want text”:
<div data-foo={(string) $foo}>
{* Renders exactly as in old Latte *}
B) The new behavior is correct (accepting the change)
You'll often find that the new behavior is actually better (e.g., that an
empty class attribute disappears). To get rid of the migration
warning, use the |accept filter. This says: “I checked it, the
change is fine.”
<div class="{$var|accept}">
{* Uses the new behavior (attribute disappears on null) and no warning is output *}
After disabling migration warnings, simply delete all
|accept from your templates.
Latte 3.1 thus brings modern standards to writing templates, cleans generated HTML from clutter, and ensures you don't accidentally output nonsense. The transition is safe and gradual thanks to migration tools.
