Flexible attributes in Latte and compatibility

David Grudl
Nette Core | 8278
+
0
-

I'd like to introduce functionality that will bring greater flexibility when working with dynamic attribute values. At the same time, I'm looking for a way to maintain full backward compatibility.

Main need: controlling attribute presence through values

In practice, we often need an attribute to be rendered or not rendered based on some condition. Especially with data-attributes, it can be much more elegant to have instead of the pair <div data-active="true"> and <div data-active="false"> this pair: <div data-active> and <div>.

Of course, this doesn't only apply to data-attributes, but to all of them. Similarly useful can be outputting title="{$title}" only when we have content, target="_blank" only for external links, etc.

But how to write this in Latte? We're essentially dealing with a scenario where:

<div foo="{$foo}"></div>
// for certain value of $foo renders: <div></div>
// for certain value of $foo renders: <div foo></div>
// for certain value of $foo renders: <div foo="..."></div>

Possible solution: null causes attribute omission

One way to achieve this is to let the attribute value null cause the entire attribute to not be rendered. That's precisely the semantic meaning of null:

<a href="/"
	title="{$title}"
	target="{$link->isExternal() ? '_blank'}"
	data-active="{$active ? ''}">
   link
</a>

// internal link without title and active
// renders: <a href="/">link</a>

// external link with active:
// renders: <a href="/" target="_blank" data-active>link</a>

For pseudo-boolean attributes (like data-active in this example), I need to make sure that the resulting value is either an empty string or null (note: from an HTML standpoint, <div foo=""> and <div foo> are identical). This can be written as data-active="{$active ? ''}", meaning when $active is truthy, return empty string, otherwise null. Perhaps it would be clearer to create a filter for this, e.g. <a data-active={$active|toggle}>.

But how to deal with this in terms of backward compatibility?

Current behavior renders null values as empty attributes (<div foo="">), while new behavior would completely hide such attributes (<div>). What would happen if the behavior with null changed?

For standard HTML attributes, the difference between an empty value and absence of the attribute isn't significant in most cases – attributes like class="", title="" or style="" behave the same as if they didn't exist at all. In these cases, omitting attributes would actually be desirable. Problematic are only specific attributes with defined semantics, for example href="" (link to current page) versus absence of href (inactive element), or alt="" (decorative image) versus absence of alt (accessibility error).

For data-attributes, the situation is much more complicated. JavaScript and CSS code often relies on the presence of these attributes using hasAttribute() or selectors like [data-foo], so changing from <div data-foo=""> to <div> could break existing functionality. We simply cannot afford such a change here.

From a backward compatibility perspective, the transition would have to be multi-phase. Latte would first have to warn about attributes containing null and the user would have to decide which ones are okay to omit and which ones to keep, and there they would add type casting to string or nullcoalesce operator alt="{$alt ?? ''}".

Given these problems, it appears that directly changing the behavior of null values isn't a viable approach. We need to find a way to achieve the same functionality without the risk of breaking backward compatibility.

Possible solution: null without quotes causes omission

What if a null value resulted in omitting the attribute only when written without quotes?

<a href="/"
	title={$title}
	target={$link->isExternal() ? '_blank'}
	data-active={$active ? ''}>
   link
</a>

It's important to note that this isn't a newly proposed syntax – writing without quotes has worked in Latte for 12 years. Based on my analysis, however, practically no one uses it, so changing the behavior wouldn't constitute a de facto BC break.

Omitting quotes can logically be interpreted as a hint that the attribute doesn't necessarily need to have a value, and following this logic, it can be completely suppressed. While writing with quotes target="{$null}" would preserve current behavior (empty attribute), writing without quotes target={$null} would completely omit the attribute.

Possible solution: different value causes omission

Another possibility is to use a different value than null that would cause attribute omission. This would entirely sidestep the problem with backward compatibility.

Latte could define a special constant, for example OMIT or SKIP:

<a href="/"
	title="{$title ?? OMIT}"
	target="{$link->isExternal() ? '_blank' : OMIT}"
	data-active="{$active ? '' : OMIT}">
   link
</a>

The benefit is that this represents completely new functionality without risk of BC break. The disadvantage is the need to remember a special constant and slightly more verbose syntax.

(Btw we can't use the value false for this purpose because it has a different special meaning)

Possible solution: new filter

Another possibility would be to add dedicated filters for conditional attribute rendering. Filter |toggle, which operates based on truthy/falsey values. And filter |optional, which omits on null:

<a href="/"
	title="{$title|optional}"
	target="{$link->isExternal() ? '_blank'|optional}"
	data-active="{$active|toggle}">
   link
</a>

The advantage of filters is their clarity of intent. However, the disadvantage is that this solution doesn't allow preparing conditional attribute omission on the PHP side – the decision whether an attribute should be rendered must always be explicitly written in the template using a filter.

What do you think about these variants? Which path would you recommend?

I would like to hear your opinions on this functionality and preferred solution for backward compatibility.