Flexible attributes in Latte and compatibility
- David Grudl
- Nette Core | 8278
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.