Oddělené vázání JavaScriptových událostí

Školení, která pořádám

Na JavaScriptových frameworcích se mi kromě jiného líbí, že vedou programátory k důslednému oddělení HTML a JS kódu. S touto technikou, která vede k čistému kódu, je ale spojen jeden opomíjený problém.

Představte si kód, který automaticky odešle formulář, když uživatel změní položku výběrového seznamu. Odesílací tlačítko je proto vhodné schovat do značky <noscript>, aby na něj uživatel neměl chuť klikat:

<select onchange="this.form.submit();"><option></option></select>
<noscript><p><input type="submit" /></p></noscript>

Kdybychom událost navázali až po načtení dokumentu, tak bude existovat chvíle (mezi zobrazením formuláře a spuštěním skriptu), kdy změna výběru formulář neodešle a zároveň nebude vidět tlačítko. Problém je, že se tato chvíle z různých důvodů může docela protáhnout.

Při odděleném navazování událostí musíme úlohu řešit jinak (využívá se podmíněného skrytí elementu při zapnutém JavaScriptu):

<select id="sel"><option></option></select>
<p class="hidden-js"><input type="submit" /></p>
<script type="text/javascript" src="jquery.js"></script>
<script type="text/javascript">
$('#sel').change(function () {
	this.form.submit();
});
document.body.className += ' javascript';
</script>

Takovéto řešení už bude spolehlivě fungovat, při načtení stránky ale problikne odesílací tlačítko – a toto „probliknutí“ se opět může protáhnout třeba na několik sekund.

jQuery 1.3 nabízí tzv. živé události, ty ale nepodporují všechny události. Ideální řešení tohoto problému bohužel neznám.

Jakub Vrána, Řešení problému, 26.6.2009, diskuse: 49 (nové: 0)

Diskuse

Mira:

Používám mootools a při události domReady příslušné tlačítko skriju (display: none). a zároveň vím že se mi zinicializují akce pro submit formuláře, to se mi zdá docela neprůstřelné, ne?

ikona Jakub Vrána OpenID:

Neprůstřelné to není, přečti si článek znovu a přidej ještě komentář od Dera.

Leo:

Tak dejte jeste jednu znacku script hned za uvodni tag body a do nej to document.body.className += ' javascript';, ne? Leo

ikona Jakub Vrána OpenID:

Tím se problém jedině zhorší, v článku to je popsáno po první ukázce kódu.

Leo:

Jeste ne napadla jedna moznost. Javascript bude az za podstatnym html kodem, v nem nebude zadne onneco definovane. A ten JavaScript naklonuje z html uzel obsahujici znacky, ktere chceme zmenit (select a input submit), v naklonovane verzi provede zmeny (definice onchange a zablokovani inputu) a provede replaceChild. To by mohlo byt docela atomicke.

ikona Jakub Vrána OpenID:

Atomické to rozhodně nebude. Jde o to, že HTML se zobrazuje průběžně ještě před spuštěním JavaScriptu po stažení stránky.

Leo:

Nevim, jestli si rozumime. Schematicky asi takhle:

<div id="kontejner">
<select ...>
<input type="submit">
<jine tagy...
</div>
<script>
var k = document.getElementById("kontejner");
var kopie = k.cloneNode(true);
[uprava kopie do js verze]
k.parentNode.replaceChild(kopie,k);
</script>

Na zacatku mate verzi bez js, ale funkcni (uzivatel odesila submit tlacitkem), pak verzi s js, kdy se formular odesila na onchange. Jinak uz priznam se nechapu co se vlastne resi...

ikona Jakub Vrána OpenID:

Tohle je bohužel k ničemu, zůstanou oba problémy:

1. Před spuštěním JavaScriptu problikne neJS verze.

2. Pokud uživatel před spuštěním JavaScriptu něco vybere, ale nestihne to potvrdit, tak se to po spuštění JS nezachytí.

Leo:

ad 1/

nevidel bych v tom problem, ale pokud jsem to pochopil z vaseho jineho komentare chcete aby JS fungoval hned. Nevim, jestli je to mozne i pokud pouzijete inline zaveseni udalosti, protoze se stejne musi cekat na nacteni potrebneho kusu kodu html (pokud chcete pracovat s elementy, ne vsechny jsou v html pred tim, kde je udalost zavesena). Takze stejne se musi cekat - ne nutne na onload, nebo na domready, ale na patricny kus kodu (treba cely formular).

ad 2/

proc by to JS nemohl zachytit?

ikona Jakub Vrána OpenID:

2. Zachytávání samozřejmě možné je, v diskusi už to tady bylo popsáno.

ikona Aichi:

Každý kdo potřebuje aby se čudlík schovával hned použije první řešení, které zmiňuješ. Každý kdo používá knihovny, protože je zmlsaný a navěšuje vše až na domready, nebo onload musí s prodlevou počítat. Nechápu co je na tom divného.

Obecně pokud potřebujeme něco hned, vlepujeme JS kód ihned za blok HTML které potřebujeme oživit.

Pokud chceme navěsit událost na onload a tlačítko skrýt hned, možná by stálo za úvahu dát od hlavičky stránky kousek JS, který skryje čudlík tak, že čudlík má třídu a této třídě nastavím např. display=none.

ikona Jakub Vrána OpenID:

Já bych chtěl zcela oddělit HTML a JS kód a přitom nezhoršit chování stránky. S prodlevou počítat nechci.

Honza Marek:

Nevím jak přesně fungují živé události v jQuery, ale ještě existuje plugin liveQuery, který podporuje všechny události a kromě toho ještě zavolání funkce nad právě vytvořeným elementem bez události.

Jestli jsem ho ale dobře pochopil, tak tento problém neřeší. Funguje snad tak, že nové elementy na které je třeba navěsit handlery zjišťuje při domReady a pak po zavolání každé funkce, která mění dom (html, attr, append, ...). Snad jsem tam nic nepřehlédl...

Dero:

Mira: Nevím, zda Vám správně rozumím, ale myslím, že problém by nastat mohl. Co když je stránka načtená z poloviny, já změním hodnotu v selectu, mezitím se stránka dotáhne a tlačítko mi zmizí? V takovém případě laik formulář neodešle.

Aichi: Myslím, že pokud potřebujeme JS funkcionalitu ihned a pokud možno s co nejmenší šancí, že se skript nenačte spolu s prvkem, je nejlepším řešením přímo použití HTML parametrů onNěco.

Jakub: Skrze brýle použitelnosti se jako nejlepší řešení nabízí vůbec skript na select nenavěšovat a nechat odeslání formuláře na uživateli. To ale není předmětem diskuse.

Přijměme premisu, že uživatel nedokáže změnit hodnotu v selectu rychleji než za 200 milisekund. Můžeme tedy ještě před začátkem načítání DOMu spustit interval, který co 200 milisekund bude testovat existenci žádaného uzlu.

Pokud již select existuje (byť v nekompletním stromu), navážeme na něj požadovanou funkci a interval ukončíme. Předtím však zkontrolujeme jeho hodnotu, zda uživatel přeci jen nějak nestihl vybrat některou z položek. Pokud ano, funkci zavoláme.

Takhle máš jistotu, že uživatelova akce vždy vyvolá správnou reakci (v nejhorším případě s téměř neměřitelnou prodlevou, v drtivé většině případě s prodlevou nulovou). Submit button můžeš vložit do noscriptu.

Test existence uzlu pětkrát do sekundy prohlížeč nezatíží vůbec nijak. V různých obměnách se dá tento postup použít i jinak. Je mi ale jasné, že programátoři takové kočkopsí řešení označí za neelegantní. Avšak problém, jak ho přednesl Jakub, nemá podle mě jiné řešení, které tak striktně odděluje prezentační a aplikační vrstvu, než se spolehnout na hraniční reakční dobu uživatele (což je bez problémů možné).

ikona Jakub Vrána OpenID:

Díky za výborný postřeh. Pokud uživatel stihne před schováním tlačítka vybrat položku, ale nestihne ji potvrdit, tak má skutečně smůlu. Je to samozřejmě ještě zásadnější problém než probliknutí tlačítka.

Tebou navrhované řešení je ale velmi krkolomné. Já jsem přemýšlel o tom, že bych na začátku skriptu definoval události, které se kam mají navěsit a output buffering v PHP by mi to poskládal do HTML kódu. Zdrojové kódy by tak zůstaly dokonale oddělené, výstup zase dokonale použitelný. Nevýhodou by pak bylo pouze to, že jde o neprůhledné řešení.

Leo:

Nechce se mi nad tim v dnesnim dusnu moc premyslet, ale je otazka, jestli v dnesni dobe sablon, frameworku, CMS a jinych udelatek je problem, pokud se ve VYSLEDNEM kodu, ktery jde do prohlizece micha JS s HTML napriklad pouzitim atributu onclick. Staci je bohate oddelit ve vyvojovem prostredi pro vyvojare, ne? Vadi nejak prudce onclick vyhledavacum apod.? Pokud vyloucime tento zpudob zaveseni ovladacu udalosti, pak bych proste obetoval vlozeni dalsiho scriptu hned za uvodni znacku body, ktera nastavi pres js patricny class co pres CSS schova a ukaze vsechno potrebne. Leo

Tomáš Fejfar:

No, vadí to z hlediska toho, že může být aktivní grafik  / kodér, který nerozumí JS a naší funkci nám z onload nebo onclick smázne :)

ikona Jakub Vrána OpenID:

Leo navrhuje (stejně jako já o příspěvek výš), že by zdrojový kód byl oddělený a až na výstupu by se spojil do jednoho. K výstupu se kodér samozřejmě už nedostane.

Helper:

Na konci článku je poukázáno na live u jQuery, který bude více událostí umět až ve verzi 1.3.3, zatím je řešením použít plugin liveQuery

ikona Jakub Vrána OpenID:

liveQuery to dělá způsobem, který popisuje Dero. Sice je to neelegantní, ale funguje to (když tedy pominu těch 20 milisekund, což je interval pro kontrolu nově přidaných elementů :-)).

Leo:

Jeste jeden dotaz - kdyz bude udalost zavesena primo v onchange atributu, nehrozi, ze uzivatel vybere jinou polozku (a dojde k odeslani formulare) jeste predtim, nez se kompletne nacte cely html kod formulare? Jinak receno odesle se neuplny, bez poslednich formularovych prvku?

ikona Jakub Vrána OpenID:

To je dobrý postřeh, přinejmenším ve Firefoxu to tak opravdu funguje. Ale není to specifické pro JavaScript, odeslání formuláře pomocí Enter ho také pošle nekompletní. Tohle by podle mě měl vyřešit prohlížeč, z kódu se to nedá nijak ošetřit.

ikona Jakub Vrána OpenID:

V kódu by se to dalo ošetřit tak, že by <form> dostal onsubmit="return false;", což bychom po zobrazení celého formuláře odstranili.

Marek:

A ted tu o andelech a spicce jehly (anebo neexistuje tisic jinych, palcivejsich problemu?)

Dero:

Myslím, že jste se zaměřil na konkrétní příklad a zcela ignorujete obecný problém, který se tu vlastně řeší.

ikona Jakub Vrána OpenID:

Ano, máš pravdu, promiň. Odteď budu na blogu řešit výhradně potlačení chudoby a hladu nebo otázku celosvětového míru (i když ve válkách postupuje pokrok nejrychleji).

Mastodont:

... Představte si kód, který automaticky odešle formulář, když uživatel změní položku výběrového seznamu. ...

A co se třeba na toto vykašlat a nechat uživatele, aby formulář normálně odeslal? Nebo to už není správně cool?

ikona Jakub Vrána OpenID:

Ale to není jenom tahle jedna věc, týká se to prakticky všech JavaScriptových událostí. Když je stránka obohacena o JavaScript a uživatel ho má zapnutý, tak je podle mě jasná chyba, že po nějakou dobu JS nefunguje.

Mastodont:

Dobře, já netvrdím, že to jen jedna věc ... jestliže to prostě může dělat potíže, proč to vůbec používat, když je možnost normálního odeslání?

Za sebe v každém případě děkuji - vím, co rozhodně nebudu kódovat.

ikona Jakub Vrána OpenID:

Důvodem pro používání je to, že se uživateli ušetří jedno kliknutí.

Navíc tohle je jen ukázka obecného problému, který se týká všech JS událostí.

Shark:

A přidá frustrace v případě, že zatočí kolečkem myši, když má focus zrovna na selectu.

ikona Jakub Vrána OpenID:

To se dá vyřešit buď nepoužíváním IE6, který tímto neduhem trpí (IE7 ani Firefox ne), nebo obsluhou události onmousewheel.

Ondřej Štoček:

A co třeba při události window.onload porovnat, jestli se aktuální hodnota selectu rovná jeho defaultní hodnotě(tusim, ze to je atribut defaultSelected) a v případě, že se nerovná, formulář rovnou odeslat?

Dero:

Podle mého názoru slepá ulička. onLoad (příp. i různé implementace domReady) však mohou nastat až třeba po několika desítkách sekund od okamžiku, kdy uživatel hodnotu změní.

Ondřej Štoček:

Upřesním: pokud stránka není komplet načtená (nenastala událost onload/domready), je zobrazen select a odesílací tlačítko. Uživatel tak může odeslat standardním způsobem. Při události onload skryji odesilací tlačítko a nastavím obsluhu události onselect. Může tak nastat situace, že uživatel změnil volbu v selectu a ještě nestihnul formulář odeslat tlačítkem, které mezi tím zmizelo.

Řešil bych to tak, že při domready kontroluji změnu hodnoty selectu oproti defaultní hodnotě a případně formulář automaticky odešlu.

Slepou uličku v tom nevidím.

Ondřej Štoček:

Samozřejmě mám na mysli obsluhu jedné události, buď domready, nebo onload. Trošku jsem je v předchozím postu pomíchal.

ikona pa3k:

Oddelovanie HTML od JS kódu je IMHO v tomto prípade kontraproduktívne. Nevidím problém v inline JavaScript-e alebo vo vložení <script> tagu kdekoľvek je potreba. Snahy o maximálnu čistotu výsledného HTML kódu a oddelenie HTML od JS za každú cenu tu IMHO nemajú zmysel. Je jedno ako vyzerá výsledný HTML kód, oveľa dôležitejšia je bezproblémová funkčnosť. Oddelenie z pohľadu tímovej tvorby (ak je potrebné) by mal mal riešiť framework/cms tak, aby bol inline JavaScript oddelený a do výsledného HTML by sa dynamicky linkoval pomocou prostriedkov frameworku/cms.

PS: kodér by IMHO mal ovládať nielen HTML ale aj JS aspoň na úrovni inicializácie ptrebných funkcií na správnom mieste.

ikona Jakub Vrána OpenID:

S tímto postojem se v zásadě ztotožňuji s tou jedinou připomínkou, že u velkých projektů se JS programátor nemá co hrabat v HTML a kodér zase nemá psát ani kousek JS (i když by si vzájemně samozřejmě měli rozumět).

Zbývá tedy vyřešit, jak spojit kód, který je ve zdrojových souborech oddělený, tak, aby to bylo co nejméně matoucí pro lidi, kteří framework/CMS neznají.

Hever:

Google své řešení předvádí např. v gmailu - naukáže nic, dokud není vše načtené.

ikona Jakub Vrána OpenID:

V čistě JavaScriptové aplikaci se to dá zařídit např. tak, že má celá stránka display: none a až na konci se jí skriptem nastaví display: block. V aplikaci, která musí fungovat i bez JS mě řešení nenapadá.

Leo:

No vzdycky muzete jeste pouzit presmerovani na verzi bez javascriptu pres meta refresh v noscript. Neni (asi) validni, ale koho to dnes krome extremistu jeste zajima.

Leo:

"V čistě JavaScriptové aplikaci se to dá zařídit např. tak, že má celá stránka display: none a až na konci se jí skriptem nastaví display: block."

Takze pokud se stranka nenacte cela neuvidi uzivatel vubec nic.

ikona Jakub Vrána OpenID:

Může to být samozřejmě i po blocích, které spolu souvisí. Tím se ale zase JS kód nebo odkaz na něj dostává do HTML :-(.

Visitor:

Nečetl jsem všechny komentáře, ale mohu form a jeho prvky načítat s vlastností display:none nebo disabled="disabled" a míta tam nějakou informaci "čekejte na načtení".

Samozřejmě je mi jasné, že paranoici s vypnutým JS mají smůlu.

Prdlořeznictví Krkovička, n. p.:

A co verze, ve které potvrzovací tlačítko se zobrazí implicitně viditelné a zneviditelní ho až JavaScriptový kód v document.onload()?

Ano, potvrzovací tlačítko bude na jistou chvíli vidět (od jeho zobrazení do zobrazení celé stránky). Ale co do funkčnosti bych řek', že je to stejné jako řešení s "hidden-js" (navíc nepotřebuje jquery.js, které navíc na některých mobilních zařízeních nepremáva).

Oaki:

Dat na stranku Loader.

<em>anonymní</em>:

Někde jsem tu viděl řešení typu:
<script type="text/javascript">
function foo()
...
</script>
...
onNěco="foo();"
Mám tu nějakou záruku, že v době řešení události něco bude proveden skript, ve kterém je foo()?

ikona Jakub Vrána OpenID:

Ano, na zpracování značky <script> se čeká. Jedinou výjimkou je použití atributu defer, to ale funguje jen v IE.

Tom:

A co třeba něco jako:
<html><head>
<script>
document.write("<style>button#PotrebujiSchovat {
display:none;
}</style>");
</script>
</head><body>

Vložit komentář

Používejte diakritiku. Vstup se chápe jako čistý text, ale URL budou převedeny na odkazy a PHP kód uzavřený do <?php ?> bude zvýrazněn. Pokud máte dotaz, který nesouvisí s článkem, zkuste raději diskusi o PHP, zde se odpovědi pravděpodobně nedočkáte.

Jméno: URL:

avatar © 2005-2018 Jakub Vrána. Publikované texty můžete přetiskovat pouze se svolením autora. Ukázky kódu smíte používat s uvedením autora a URL tohoto webu bez dalších omezení Creative Commons. Můžeme si tykat. Skripty předpokládají nastavení: magic_quotes_gpc=Off, magic_quotes_runtime=Off, error_reporting=E_ALL & ~E_NOTICE a očekávají předchozí zavolání mysql_set_charset. Skripty by měly být funkční v PHP >= 4.3 a PHP >= 5.0.