Flexibilní atributy v Latte a kompatibilita

David Grudl
Nette Core | 8284
+
+3
-

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
+
+2
-

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
+
0
-

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
+
+1
-

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
+
-3
-

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
+
+2
-

@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
+
+3
-

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 odkazu
  • crossorigin="" 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í.