Flexibilní atributy v Latte a kompatibilita
- David Grudl
- Nette Core | 8284
Chci představit funkcionalitu, která přinese větší flexibilitu při práci s dynamickými hodnotami atributů. Přitom hledám cestu, jak zachovat plně zpětnou kompatibilitu.
Hlavní potřeba: ovlivňovat přítomnost atributů pomocí hodnoty
V praxi často potřebujeme, aby se atribut vykreslil nebo nevykreslil podle
nějaké podmínky. Zvláště u data-atributů může být
mnohem elegantnější mít místo dvojice
<div data-active="true">
a
<div data-active="false">
tuto dvojici:
<div data-active>
a <div>
.
Samozřejmě se to netýká jen data-atributů, ale všech. Podobně
užitečné může být vypsat title="{$title}"
jen pokud máme
obsah, target="_blank"
jen u externích odkazů atd.
Ale jak to zapsat v Latte? Vlastně řešíme úkol, kdy:
<div foo="{$foo}"></div>
// při určité hodnotě $foo vykreslí: <div></div>
// při určité hodnotě $foo vykreslí: <div foo></div>
// při určité hodnotě $foo vykreslí: <div foo="..."></div>
Možné řešení: null
způsobí nevykreslení atributu
Jedna z možností, jak toho dosáhnout, je nechat hodnotu atributu
null
způsobit, že se celý atribut nevykreslí. Protože fakticky
přesně to je sémantický význam null
:
<a href="/"
title="{$title}"
target="{$link->isExternal() ? '_blank'}"
data-active="{$active ? ''}">
odkaz
</a>
// interní odkaz bez titulku a active
// vykreslí se: <a href="/">odkaz</a>
// externí odkaz s active:
// vykreslí se: <a href="/" target="_blank" data-active>odkaz</a>
V případě pseudo-boolean atributů (jako data-active
v této ukázce) se musím postarat, aby výsledná hodnota byla buď prázdný
řetězec nebo null (poznámka: z pohledu HTML je
<div foo="">
a <div foo>
totéž). Což se
dá zapsat právě jako data-active="{$active ? ''}"
, tedy když je
$active
truthy, vrať prázdný řetězec, jinak null
.
Možná by bylo srozumitelnější na to udělat nějaký filtr, např.
<a data-active={$active|toggle}>
.
Ale jak to pořešit vzhledem ke zpětné kompatibilitě?
Současné chování vykresluje null
hodnoty jako prázdné
atributy (<div foo="">
), zatímco nové chování by takové
atributy úplně skrylo (<div>
). Co by se stalo, kdyby se
chování při null
změnilo?
U standardních HTML atributů není rozdíl mezi prázdnou
hodnotou a absencí atributu ve většině případů významný – atributy
jako class=""
nebo style=""
se chovají stejně, jako
kdyby vůbec neexistovaly. V těchto případech by bylo vynechání atributů
vlastě žádoucí. Problematické jsou jen specifické atributy s definovanou
sémantikou, například href=""
(odkaz na aktuální stránku)
versus absence href
(neaktivní prvek), nebo alt=""
(dekorativní obrázek) versus absence alt
(accessibility
chyba).
U data-atributů je situace mnohem komplikovanější.
JavaScript a CSS kód často spoléhá na přítomnost těchto atributů pomocí
hasAttribute()
nebo selektorů typu [data-foo]
, takže
změna z <div data-foo="">
na <div>
by
mohla rozbít existující funkcionalitu. Tady si podobnou změnu vůbec
dovolit nemůžeme.
Z hlediska zpětné kompatibility by přechod musel být vícefázový.
Latte by muselo nejprve na atributy obsahující null
upozornit a
uživatel by se musel rozhodnout, které je ok vynechat a které chce zachovat,
a tam by přidal přetypování na string nebo nullcoalesce operátor
alt="{$alt ?? ''}"
.
Vzhledem k těmto problémům se zdá, že přímá změna chování
null
hodnot není praktická cesta. Potřebujeme najít způsob,
jak dosáhnout stejné funkcionality bez rizika porušení zpětné
kompatibility.
Možné řešení: null
bez uvozovek způsobí
nevykreslení
Co kdyby hodnota null vedla k vynechání atributu jen tehdy, pokud je zapsán bez uvozovek?
<a href="/"
title={$title}
target={$link->isExternal() ? '_blank'}
data-active={$active ? ''}>
odkaz
</a>
Důležité je, že toto není nová navrhovaná syntax – zápis bez uvozovek funguje v Latte již 12 let. Podle mých analýz ho však prakticky nikdo nepoužívá, takže by změna chování neznamenala de facto BC break.
Vynechání uvozovek se dá logicky chápat jako
naznačení, že atribut hodnotu vůbec nemusí mít, a dalším
pokračováním této myšlenky je, že může být úplně potlačen. Zatímco
zápis s uvozovkami target="{$null}"
by zachoval současné
chování (prázdný atribut), zápis bez uvozovek target={$null}
by atribut úplně vynechal.
Možné řešení: jiná hodnota způsobí nevykreslení
Další možností je použít jinou hodnotu než null
, která
by způsobila nevykreslení atributu. Tím by se úplně vyhnul problém se
zpětnou kompatibilitou.
Latte by mohlo definovat speciální konstantu, například OMIT
nebo SKIP
:
<a href="/"
title="{$title ?? OMIT}"
target="{$link->isExternal() ? '_blank' : OMIT}"
data-active="{$active ? '' : OMIT}">
odkaz
</a>
Výhoda je, že jde o úplně novou funkcionalitu bez rizika BC breaku. Nevýhoda je nutnost pamatovat si speciální konstantu a o něco delší zápis.
(Btw hodnotu false
pro tento účel použít nemůžeme,
protože má jiný speciální
význam)
Možné řešení: nový filtr
Další možností by bylo přidat specializované filtry pro podmíněné
vykreslování atributů. Filtr |toggle
, který se orientuje se
podle truthy/falsey hodnoty. A filtr |optional
, který vynechá
při null:
<a href="/"
title="{$title|optional}"
target="{$link->isExternal() ? '_blank'|optional}"
data-active="{$active|toggle}">
odkaz
</a>
Výhoda filtrů je jejich jasnost záměru. Nevýhodou však je, že toto řešení neumožňuje připravit podmíněné vynechání atributu na straně PHP – rozhodnutí o tom, zda se atribut vykreslí, musí být vždy explicitně zapsáno v šabloně pomocí filtru.
Co si myslíte o těchto variantách? Kterou cestu byste doporučili?
Rád bych slyšel vaše názory na tuto funkcionalitu a preferované řešení zpětné kompatibility.
- Forrest79
- Backer | 9
A co to nové chování atributů prefixovat/postfixovat otazníčkem?
Místo:
<a href="/"
title="{$title ?? OMIT}"
target="{$link->isExternal() ? '_blank' : OMIT}"
data-active="{$active ? '' : OMIT}">
odkaz
</a>
By bylo:
<a href="/"
title?="{$title}"
?target="{$link->isExternal() ? '_blank'}"
data-active?="{$active}">
odkaz
</a>
Nebylo by potřeba pamatovat si speciální syntax a ten otazníček je tam celkem i návodný. Člověk už to zná z anotací array shape u PHPStanu, u klíčů, které tam můžou nebo nemusí být.
Na druhou stranu chápu, jak bylo super se zbavit !
jako
noescape
:-)
- Pavel Kravčík
- Člen | 1206
OMIT je asi lepší, než filtr. Asi bych taky šel cestou, že by se měl modifikovat ten atribut napřímo (ne atribut dle dat).
Možná, nějaký nový "n:"
, něco jako
sn: /skip if null sb: /skip if bool
:
<a href="/"
sn:title="{$title}"
sn:target="{$link->isExternal() ? '_blank'}"
sb:data-active="{$active}">
odkaz
- Kamil Valenta
- Člen | 844
David Grudl napsal(a):
Výhoda filtrů je jejich jasnost záměru. Nevýhodou však je, že toto řešení neumožňuje připravit podmíněné vynechání atributu na straně PHP – rozhodnutí o tom, zda se atribut vykreslí, musí být vždy explicitně zapsáno v šabloně pomocí filtru.
To já bych vnímal spíše jako výhodu. Backend programátor nemusí / nechce tušit, zda je potřeba atribut vykreslit. Naopak je to rozhodující pro frontend kodéra, který má třeba přístup jen k šablonám, tak si sám určí filtrem, jak se to má chovat. A ono někdy může být potřeba null interpretovat jako prázdný atribut a jindy (avšak ve stejném kontextu) jako prvek bez atributu. Neměla by v tom být tvrdá automatika.
Mnohem více by mi u filtrů vadilo, že při
title="{$title|optional}"
filtr |optional neovlivní jen $title, jak je u filtrů zvykem, ale sáhne za kontext složených závorek. To je důvod, proč bych to zavrhl, i když se mi to zdá jinak nejlepší varianta…
Nějak jsem nepochopil, proč se myšlenky odvrací od již existujícího n:attr. Nový sn: by dělal prakticky to samé. Není lepší tedy jen n:attr zdokonalit, pokud u něj něco nevyhovuje? Nebo rozšířit, ale v podobném duchu n:něco…
Editoval Kamil Valenta (3. 7. 8:47)
- kminekmatej
- Generous Backer | 39
Mě se líbí zápis bez uvozovek:
{var $title=null}
<span title={$title}> ... <span>
zatímco
{var $title=null}
<span title="{$title}"> ... <span title="">
Tím že ty uvozovky v zápisu uvedu jasně deklaruji že tam ty uvozovky prostě chci a basta
- m.brecher
- Generous Backer | 905
@kminekmatej
Mě se líbí zápis bez uvozovek…
V Latte je zvykem tolerovat zápis hodnot bez uvozovek a chápat je stejně jako by byly s uvozovkami, např.:
{block 'title'}
{block title}
takže ovlivňovat uvedením/neuvedením uvozovek jak se hodnota vykreslí je v Latte syntakticky problematické.
mě by se osobně líbilo, kdyby pro vybrané, často používané atributy, zřejmě především id, title by se zavedla n: varianta plně v souladu s již existujícími pravidly:
<p n:id="$id"></p>
<p n:title="$title"></p>
Výhody – žádná nová syntaxe, intuitivní. Zavádět varianty pro další, okrajově používané atributy není nutné, protože tam lze v případě nutnosti použít n:attr.
Editoval m.brecher (3. 7. 22:34)
- David Grudl
- Nette Core | 8284
U standardních HTML atributů jsem udělal poctivou analýzu případů, kdy je smysluplný rozdíl mezi prázdnou hodnotou a nepřítomností:
Rozdíly jsou tyto:
title=""
explicitně prázdný tooltip potlačí výchozí (nadřazený) tooltip<a href="">
vytvoří odkaz na aktuální stránku oproti žádnému odkazucrossorigin=""
je ekvivalentní k crossorigin=„anonymous“, nepřítomnost znamená že CORS se nepoužije<a download="">
– soubor se stáhne s jeho původním názvem ze serveru<img alt="">
obrázek je dekorativní a uživatelé asistivních technologií ho budou ignorovat<iframe sandbox="">
aktivuje nejpřísnější sandbox omezení, neuvedení znamená žádná omezení<option value="">
má prázdnou hodnotu, jinak používá svůj textový obsah jako hodnotu
Pouze v případě download
a value
mi dává
smysl, že by ho měl uživatel zapsaný v Latte s proměnnou a spoléhal se
na chování s prázdnou hodnotou při null (byť u toho
<option>
je pravděpodobnější, že uživatel používá
Nette Forms). Ostatní případy mi nedávají úplně smysl. Zároveň existuje
velká spousta standardních atributů, kde je prázdná hodnota nevalidní.
(Existence problematického atributu se dá oveřit hledáním regulárního
výrazu např. download=["']?\{
)
Takže z pohledu standardních atributů mi připadá změna chování null proveditelná.
Diametrálně odlišná situace je u datových atributů.
Nicméně tady si říkám, že by bylo pro mě vlastně nejvíc užitečné,
kdybych si mohl zvolit, jak se bude chovat u nich null a také
boolean. Protože pro mnoho lidí by mohlo být velice fajn, aby se true a false
vypisovalo jako data-foo="true"
a data-foo="false"
.
Nebo jako data-foo
vs nic. A tak dále.
Tím se dostávám k myšlence, že bych to možná vyřešil tak, že by v případě datových atributů bylo možné definovat funkci, tedy jakýsi standardní filtr, který by se na každou hodnotu aplikoval. Standardní chování by bylo to současné, protože kompatibilita, ale mohlo by se snadno změnit na více vyhovující.