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

David Grudl
Nette Core | 8292
+
+5
-

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:

  1. Greater convenience and flexibility: To be able to write <div foo="{$val}"> and have Latte handle it intelligently
  2. Control over attribute existence: So that the value decides not only the content but also whether the attribute is rendered at all
  3. Type checking: So that Latte warns you when you try to output nonsense (e.g., an array into title)
  4. 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:

  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.
  2. Boolean attributes (checked…): Expect anything.
  3. 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 $var is null. (Previously class="", 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.

David Grudl
Nette Core | 8292
+
0
-