Komponenty s pohledy + funkční AJAX – best practice?

Upozornění: Tohle vlákno je hodně staré a informace nemusí být platné pro současné Nette.
Zax
Člen | 370
+
0
-

Zdravím,

už delší dobu se s tím trápím – chybí mi pohledy, resp. možnost změnit pohled uvnitř komponenty (neboli nemluvím o {control someControl:foo} → renderFoo).

V komponentách tvořím vše a chtěl bych mít možnost mít komponentu, která má default pohled a k tomu možnost přepnout (kliknutím na odkaz/tlačítko přímo v komponentě) třeba na edit – tedy zobrazí se třeba nějaký formulář. Ideálně kdyby to fungovalo stejně s AJAXem i bez něj. Ideálně aniž bych musel presenter znečišťovat nějakým obslužným kódem. V současné implementaci se mi nabízí dvě „řešení“:

  1. Použít persistentní proměnné a v metodě render si načíst příslušnou šablonu.

Jaké jsou s tím problémy:
Ajaxový a neajaxový odkaz se budou lišit – bez ajaxu jen odkazem změním persistentní parametr, abych se vyhnul nadbytečnému redirectu (na stavovou URL), pro ajax naopak musím vytvořit signál (třeba handleChangeView), jinak není možné komponentu invalidovat. Je trochu problém jak řešit když je Ajax zapnutý v komponentě, ale uživatel má JS vypnutý – pak bude vždy používat signál a nadbytečný redirect. Také je třeba nezapomenout na bezpečnost, aby nešlo naincludovat nějakou cizí šablonu pomocí ../../.. (víte co myslím).

  1. Použít signály.

Jaké jsou s tím problémy:
Žádná stavová URL, všechny „pohledy“ mají URL ve tvaru ?do=. K tomu je navíc potřeba celkem dost hackovat – například se blbě řeší případ, že v jednom signálu si otevřu formulář a pak jej submitnu – což mě pochopitelně dostane na jiný signál a formulář se znovu nezobrazí (co když selhala validace?). U všech formulářů je třeba dělat jakési $form->onError[] = redirect na předchozí signál. Chování ajax vs no ajax je celkem nepředvídatelné, pokud se na to zapomene. Tento způsob zatím používám, protože se mi celkem osvědčil, ajax vs no ajax se chová relativně konzistentně (vždy se použije signál, mohu tedy invalidovat), ale tvořit takto komponenty je mnohem méně praktické, než by to mohlo být, kdyby existovaly pohledy. A taky mi to nepřijde jako úplně best practice, kvůli té URL.

Zkoušel jsem i LookoutControl, ale ani ten to nemá úplně vyřešené k mé spokojenosti.

Nevěřím, že pohledy v komponentách jsou bad practice, spíš je to způsobené tím, že komponenty jsou jak hodně opižlané presentery – v podstatě jdou použít jen signály a persistentní proměnné a jinak nic. Každý určitě měl někdy potřebu udělat třeba editační tlačítko a zobrazit editační formulář v jedné komponentě a určitě se najde i někdo (jiný než já), kdo se to následně pokoušel zajaxovatět.

Já bohužel nepřišel na nic, co by nevyžadovalo extrémní hackování, a i s hacky je to celkem zabugované a ne příliš použitelné. Vím, že se to tu řešilo už dřív, ale nikdy se to úplně nedořešilo. Chtělo by to trochu víc posunout komponenty směrem k presenterům, ne moc, jenom trošku :-)

Jak to tedy řešíte vy, profíci?

Editoval Zax (8. 6. 2014 20:03)

Zax
Člen | 370
+
0
-

Tak buď nikdo nezkoušel tvořit složitější ajaxové aplikace pomocí komponent a nebo je můj příspěvek prostě tak dlouhý, že se jej nikdo neobtěžoval číst… :-(

David Matějka
Moderator | 6445
+
0
-

Asi nejlepsi a funkcni reseni by byl persistentni parametr + signal, ktery by invalidoval v pripade ajaxu respektive presmeroval na this v pripade ne-ajaxu.

Zax
Člen | 370
+
0
-

@matej21: Dík za odpověď!

Bohužel mi to přijde hodně problematické právě kvůli tomu redirectu. Polovina odkazů v aplikaci pak potřebuje dva requesty místo jednoho, což je sice v pořádku pokud jde skutečně o nějakou akci (odeslaný formulář), ale dost mi to „smrdí“ v situaci, kdy je třeba skutečně jen změnit pohled. A když neprovedu redirect, tak v url zůstane parametr „do“. A pomocí PHP nezjistím, zda má uživatel povolený JS, abych mu dyžtak podstrčil url rovnou s tím persistentním parametrem.

Taky je třeba, jak jsem již zmiňoval, si ručně ošetřit bezpečnost, mít třeba nějaký whitelist povolených pohledů, jinak si může kdokoliv přes persistentní parametr naincludovat jakoukoliv šablonu.

Fakt mi přijde divný, že tenhle problém tu mám snad jenom já, a přitom na to narážím zas a znovu, ať už jde o nějaké „inline“ zobrazení editačního formuláře nebo třeba stránkovadlo. Vždy se musím rozhodnout – buď natvrdo žádný ajax, nebo dva requesty pokud má náhodou uživatel vypnutý JS.

Zax
Člen | 370
+
0
-

Tak jsem si přes noc trochu hrál a snad se mi i relativně povedlo dosáhnout požadovaného efektu. Ještě uvidím časem, jaké s tím budou problémy, až to budu zkoušet reálně používat… Nicméně je skutečně třeba dost hackovat, navíc nepoužívám vlastní výjimky (lenost) a nejspíš tam mám i nějaký nadbytečný kód (zkoušel jsem všechno možné a úklidem jsem se moc neobtěžoval), ale i tak jsem se rozhodl, že to zveřejním. Určitě to ale není ukázkový příklad jak programovat :-D

BTW je to můj úplně první pokus něco nahrát na github.

https://github.com/…/Control.php

Jde v podstatě o pokus sjednotit načítání šablon a usnadnit práci s AJAXem (IMHO momentálně dvě největší slabiny komponent). Rozhodně to není ani zdaleka dokonalé a je třeba myslet na některé věci:

  1. Metody render* byly nahrazeny metodami beforeRender* (pořád je tedy možné použít {control komponenta:foo} ⇒ beforeRenderFoo)
  2. Parametry v beforeRender* metodách nefungují (osobně jsem je ani nikdy nepoužil – mají reálné využití?)
  3. Je třeba přetížit metodu attached a volat v ní redrawControl (jinak nefungují AJAXové odkazy na pohledy)
  4. Je třeba v presenteru (nebo nadřazené komponentě) zavolat $this['nazevkomponenty'];, jinak nefunguje AJAXový odkaz na defaultní pohled (komponenta se nevytvoří včas, pokud se jí neposílají žádné parametry)
  5. Místo opisování věčného if($this->presenter->isAjax())... jsem přidal metodu go($destination, $args=[], $snippets=[]). Snippety se překreslí pouze pokud $destination je ‚this‘, jinak se prostě použije forward. Snippety ani není třeba uvádět, pokud je „ohackovaná“ metoda attached.

Jinak to funguje celkem přímočaře:

  • Komponenta má persistentní parametr view, jehož defaultní hodnota je „Default“, do šablony se automaticky vkládá proměnná $view
  • Komponenta má metodu enableAjax, do šablony se automaticky vkládá proměnná $ajaxEnabled
  • Odkazy na pohledy se tvoří klasicky pomocí n:href="this, view=>pohled"
  • Při vykreslování se nejdříve hledá metoda viewNazevPohledu – metoda musí existovat (klidně prázdná), jinak se vyhodí výjimka (security reasons)
  • Poté se hledá metoda beforeRenderNazevRenderu ({control komponenta:foo} ⇒ beforeRenderFoo(), {control komponenta} ⇒ beforeRender()), ta v podstatě slouží k předání proměnných do šablony.
  • Pak se šablona vykreslí.
  • Šablony se hledají pomocí jednoduchého vzorce ./NazevPohledu[.NazevRenderu].latte (chování by mělo jít změnit přetížením metody getTemplatePath($view, $render))

Takový malý trapný příklad najdete ve složce example

Třeba to někomu bude připadat užitečné a já určitě uvítám jakékoliv nápady jak to ještě víc vylepšit.

akadlec
Člen | 1326
+
0
-

Mno nevím zda jsem úplně pochopil co ti to dělá. Já jsem něco podobného řešil. Nějake viewXY jsem teda nedělal, mám klasika render, renderXY metody. Základní render metoda dělá prostý render, ty rozšířené mění šablonu a pak vykreslí normal rendererem. Pak mám k dispozici ještě beforeRender kde můžu připravit nějaké data než se bude renderovat ale to více méně postrádá smysl.

Zax
Člen | 370
+
0
-

Metoda __call odchytí volání (neexistující) metody renderXY a místo toho zavolá run(‚XY‘), která pak volá viewNazevpohledu() (což NENÍ XY) a beforeRenderXY(). Zvolil jsem beforeRenderXY právě proto, aby si komponenta mohla to volání odchytit a nějak zautomatizovat. Já renderXY, resp {control komponenta:XY} považuji spíš jako způsob zobrazení než jako vyloženě pohled – na pohledy bych chtěl být schopen se nějak odkázat i přímo z komponenty, což pokud vím nejde jinak, než přes persistentní parametry. V presenteru to jde normálně, komponenty jsou o toto v základu ochuzeny. Doufám, že jsem to napsal srozumitelně..

Je to experiment, který jsem dal dohromady přes noc, třeba ještě časem vymyslím něco lepšího ;-)

EDIT:
Můžeš mít třeba komponentu „abc“, která má dva způsoby zobrazení – ten defaultní bez názvu {control abc} a třeba kompaktní {control abc:compact}. Toto umí komponenty už v základu, no problem. A k tomu chci navíc přidat podporu pohledů, tedy žádné {control abc:compact}, ale prostě podobně jako mají presentery různé action* a render* metody s tím, že se na ně můžeš jednoduše odkázat pomocí makra {link}. S oddělenými šablonami a vyřešenou bezpečností (aby nějakej trouba nenacpal do persistentního parametru v URL nějaké ‚../../../adminpanel‘). To už se řeší hrozně blbě, protože potřebuji nějaký životní cyklus, který mi to všechno pořeší automaticky. Když použiješ {control abc}, tak se automaticky volá $this[‚abc‘]->render() a když tuto metodu vytvoříš, tak se nemůže zavolat pomocí __call, ale musel by sis opět všechno řešit ručně. Proto beforeRender (což mi přišlo nejlogičtější vzhledem k tomu, že k renderování dojde hned po zavolání této metody). A pohled je jen persistentní parametr, který v tom životním cyklu volá viewNazevpohledu ještě před tím beforeRender (opět ze stejných důvodů se to nemůže jmenovat renderNazevpohledu – uvažoval jsem o actionNazevpohledu, ale view mi přijde logičtější).

EDIT2: hmm to jsem tomu asi moc nepomoh… sry.. možná si spíš projdi ten zdroják, je možná i stručnější a výstižnější než tenhle popis.

EDIT3: jo a tu metodu signalReceived jsem možná ani nemusel přetěžovat, vypadá to jako zbytek po nějakém neúspěšném pokusu, tuším že mi nefungovalo generování odkazů.

Editoval Zax (10. 6. 2014 10:42)

akadlec
Člen | 1326
+
0
-

tak renderXY si taky odchytávám v __call a buď ta metoda existuje tak se zpracuje a nebo neexistuje a provede se jen změna šablony a normální render. Jako asi chápu o co se snažíš ale zatím nevím kde si to reálně představit.

Zax
Člen | 370
+
0
-

Když ta metoda existuje, tak jak ji můžeš odchytit přes __call? To se tak trochu vzájemně vylučuje, ne?

Jinak myšlenka je celkem prostá: Komponenty používám na vše a velkou část z nich chci mít ajaxově. Pochopitelně musí fungovat stejně i když má uživatel vyplý JS. A pohledy chci k tomu, aby komponenta uměla nejen zobrazit nějakou část stránky, ale aby třeba měla v sobě nějaké tlačítko „Upravit“, které v rámci té komponenty změní pohled a zobrazí nástroje na editaci (formulář, subkomponenty apod.).

Jistě, jde toho docílit i s normální komponentou, ale nechci psát v každé komponentě obslužný kód, který mi vytáhne příslušnou šablonu apod.

EDIT: asi sem blbě pochopil.. beru zpět první odstavec.. nicméně když ta metoda existuje a rovnou se zavolá, tak se přece ochudíš o tu možnost nechat si šablony načítat automaticky apod. (prostě žádný „životní cyklus“).

Editoval Zax (10. 6. 2014 11:49)

akadlec
Člen | 1326
+
+1
-

jo asi tě chápu, taky jsem to asi na jednom místě takto potřeboval, kdy jsem měl výpis a pak sem chtěl v komponentě dělat i editaci záznamu, tak jsem to řešil přes signál a pravda moc se mě to nelíbilo, tak jsem pak komponenty začal dělit na samostatné funkční celky, takže jedna komponenta je na výpis a jiný je na editaci/přidání a nad tím celým stojí presenter. Pokud tě tedy chápu dobře tak ty v presenteru jen vytvoříš komponenty a celou práci necháváš na nich je tak?

Zax
Člen | 370
+
0
-

Přesně tak, tuto obsluhu chci mít přímo v komponentě. Možná to není best-practice, ale může to ušetřit dost psaní a znovupoužitelnost třeba v jiném projektu je mnohem jednodušší.

Signály jsou naprd z toho důvodu, že když odešleš formulář, tak tě to automaticky hodí na jiný signál a formulář se znovu nezobrazí, pokud třeba došlo k nějaké chybě. Pak člověk musí dělat různé kraviny typu $form->onError[] = přejdi na příslušný signál aby to fungovalo jak má. Pohledy by tohle měly řešit.

akadlec
Člen | 1326
+
0
-

Hele a ty tedy pro každou komponentu uděláš „layout“ ? do kterého pak dostáváš ty „view“ ? Bylo by fajn kdyby se k tomu vyjádřil některý z místních guru co na to říká.

Zax
Člen | 370
+
0
-

Nevidím problém… v presenterech se layout používá naprosto běžně, tak proč ne v komponentách? Však je to fičura šablonovacího systému a vede to k DRY. BTW používám jen tam, kde je to vhodné – například když chci mít ve všech pohledech stejné ovládací prvky, nebo jiné společné prvky…

Editoval Zax (10. 6. 2014 18:49)