Komunikace mezi komponentami již naposled
- Fires
- Člen | 96
Zdravím, procházel jsem forum a dokumentaci ale stejně z toho nejsem moc
moudrý. Uvedu názorný příklad:
Presenter vytváří 3 komponenty řekněmě tabulku, info okno a
třeba graf.
Tabulka při kliknutí musí nastavit proměnou do komponenty info okna a ta se překreslí. Při kliknutí v info okně zase musím poslat novou proměnou do grafu a ten překreslit.
Moje otázka teď zní, jak spolu mají jednotlivé komponenty, které jsou na sobě závislé komunikovat?
Možnosti které mě napadly(neotestováno):
- komunikaci bude řešit presenter, komponenty budou volat signály presenteru handleSetInfoWindow($itemId), ta se uloží do privatní proměné a ta se použije při vytvoření komponenty info okna. A tak dále.
- komunikaci bude řešit komponenta, ta si od presenteru vytáhne požadovanou komponentu a do ní nastaví proměné takže v komponentě bude následující. (tohle mi příjde jako úplná blbost, už jen kvůli netransparentnosti a nulového znovupoužití.
handleRowClick($itemId){
$this->presenter['infoWindow']->setId($itemId);
}
- „obalovací komponenta“ prakticky vytvořit obalující komponentu pro všechny komponenty a handle mít umístněné tam. (prakticky stejný přístup jak bod 1. ale nebude presenter tak zaneřáděný)
- events v komponentě
class Component1 extends Nette\Application\UI\Control
{
public $onAction = [];
public function handleAction()
{
$this->onAction();
}
}
class MyPresenter extends Nette\Application\UI\Presenter
{
protected function createComponentComponent1()
{
$component1 = new Component1();
$component1->onAction[] = function () {
$this['component2']->doSomething();
};
return $component1;
}
protected function createComponentComponent2()
{
return new Component2();
}
}
Je mi jasné že asi neexistuje jednotný přístup a každému se pracuje lépe jiným způsobem. Který přístup tedy ale nejvíce odpovídá filozofii frameworku?
Předěm mocrát díky za názory.
- mskocik
- Člen | 60
Ja osobne používam 1) a 4) v podobných prípadoch (napr. úpravy v modal
formulári prekreslia susedný grid apod.). Príde mi, že 4) je
prehľadnejšia, ak všetky takéto eventHandlery definuješ v
createComponent
metódach – čiže máš to na jednom
mieste.
Pre jednoduchú logiku (ako píšeš, že setuješ ID apod.), alebo ak nevidím význam definovaž custom eventy (nebude sa používať inde) používam 1) – menej práce a komponenta neobsahuje jednoúčelový event. Toť môj názor :)
- Kcko
- Člen | 467
K bodu 1) můžeš si udělat Traitu, a pak to nebude na první pohled
zaneřáděné a můžeš to použít i na jiných místech
K bodu 3) to je ok, když jsi si jistý, že ty komponenty budou vždy takhle
u sebe, abys je mohl mít v jedné obalující wrapper komponentě
K bodu 4) Taky existujou „globální listenery“ přes Kdyby/Events, pak
nemusíš mít ty eventy v továrníčkách, ale jako separátní třídy a
přídavat / odebírat nezávisle.
A jak říkáš, zavisí to na logice použití, případ od případu.
Editoval Kcko (26. 7. 18:30)
- Marek Bartoš
- Nette Blogger | 1230
Pokud jedna komponenta nepatří do druhé, tak logické místo kde propojení řešit je komponenta, která jim je nadřazená – kupříkladu ten presenter. Takže bych použil tvůj poslední příklad – callback definovaný v presenteru a přiřazený do komponenty.
V komponentách globální eventy rozumně nefungují – neví nic o tom, jak má být komponenta inicializovaná, protože to není služba, ale třída, která se skrze službu (továrnu) vyrábí. Též bys narazil na problém v okamžiku, kdy tu komponentu vytváříš vícekrát.
- Kcko
- Člen | 467
Marek Bartoš napsal(a):
Pokud jedna komponenta nepatří do druhé, tak logické místo kde propojení řešit je komponenta, která jim je nadřazená – kupříkladu ten presenter. Takže bych použil tvůj poslední příklad – callback definovaný v presenteru a přiřazený do komponenty.
V komponentách globální eventy rozumně nefungují – neví nic o tom, jak má být komponenta inicializovaná, protože to není služba, ale třída, která se skrze službu (továrnu) vyrábí. Též bys narazil na problém v okamžiku, kdy tu komponentu vytváříš vícekrát.
Ahoj,
rozhodně tu s tebou nebudu přít ani dohadovat, jsi přeci jen
zkušenější, nicméně v našem systému mám shluk komponent, které
obsluhují „uživatele“. Registraci, obnovu hesla atd. K tomu jsou modely a
jakýsi UserEventManager (ve kterém jsou nadefinovány eventy), tento model se
injectuje do komponent a v nich se na specifických místech eventy
provolávají.
A k tomu mám kupu Kdyby Listenerů, co se stane např. po registraci uživatele
- pošle mu to email / nepošle
- rovnou ho to přihlásí / nepřihlásí
- zařadí ho do to nějaké user skupiny
- etc.
Eventy si na projektu poté jen zaregistruji v neonu a nakonfiguruji.
Přijde mi to jako hezké a čisté řešení a pořád to jsou globální listenery a komponenty … :-) viz výše tvoje tvrzení.
- Pavel Kravčík
- Člen | 1191
Také se mi nejvíc líbí čtyřka. Netuším proč, ale ve svých projektech ještě vždycky obaluji tu závislost Comp1 a Comp2 do nějaké nadřazené komponenty, která často ani nedělá nic jiného. Je to identické jako ten presenter, jen mi přijde přehlednější.
- m.brecher
- Generous Backer | 814
@Fires
Presenter vytváří 3 komponenty řekněmě tabulku, info okno a třeba graf. Tabulka při kliknutí musí nastavit proměnou do komponenty info okna a ta se překreslí. Při kliknutí v info okně zase musím poslat novou proměnou do grafu a ten překreslit.
Když dám stranou graf, tak je to typická funkce editace databázové tabulky a) tabulku vypsat v akci presenteru do šablony, b) po kliknutí na řádek tabulky pomocí ajax snippetu do téže šablony vykreslit dynamicky panel, kde by byl třeba editační formulář daného řádku, nebo něco podobného (graf, obrázek, …), c) po editaci v panelu zajistit aktualizaci řádku v tabulce + možnost přidat nový záznam.
Já to řeším nějak takhle:
Tabulka a editační panel
Otevření/zavření modálního okna řeším analogicky jako kdyby se okno otevíralo do samostatné akce presenteru, protože ale parametr action je již použit, používám parametr $subAction + pro editovaný řádek parametr $subId.
Routa:
$router->addRoute('test/default/<subAction>[/<subId>]', 'Test:default')
Presenter:
final class TestPresenter extends UI\Presenter
{
private ?string $subAction = null;
private ?int $subId = null;
public function actionDefault(?string $subAction = null, ?int $subId = null): void
{
$this->subAction = $subAction;
$this->subId = $subId;
$this->template->subAction = subAction;
if($this->isAjax()){
$this->redrawComponent('testPanel');
}
}
public function createComponentTestForm(): Form
{
$form = new Form();
.......
$form->addSubmit('send');
$form->onSuccess[] = fn() => $this->redrawControl('testTable');
return $form;
}
}
Tabulka – v šabloně default.latte obalí se celá snippetem – překreslí se i přidání nového záznamu
<table>
<tr>
<th>....</th>
....
</tr>
{snippet 'testTable'}
{foreach $rows as $id => $row}
<tr>
<td>
<a n:href="'this', subAction: 'update', subId: $id" class="ajax">{$row->title}</a>
</td>
</tr>
{/foreach}
{/snippet}
</table>
Modální okno (panel) umístí se do snippetu v šabloně default.latte, vykreslení/nevykreslení řídí parametr $subAction:
{snippet 'testPanel'}
{if $subAction === 'create' || $subAction === 'update'}
{control 'testForm'}
{/if}
{/snippet}
Odkaz pro otevření modálního panelu pro nový záznam:
<a n:href="'this', subAction: 'create'" class="ajax">{$row->title}</a>
Odkaz pro zavření modálního panelu:
<a n:href="'this', subAction: null" class="ajax">{$row->title}</a>
Kód který je výše cca zajišťuje ajaxové otevírání a zavírání panelu, vytvoření komponenty do panelu – formuláře, předpokládá použití ajaxové knihovny Naja a ajaxové odesílání formuláře.
A jsme u hlavní otázky – jak vytvořit komponentu/y a jak provést komunikaci mezi komponentami a presenterem. Zde nabízí Nette více cest.
Já používám formulář Form zapouzdřený do samostatné komponenty UI\Control, protože:
- používám univerzální vykreslovací šablonu
- pro všechny formuláře používám abstraktního předka, který řeší znovu se opakující funkce
- projekt je přehlednější, každá komponenta je v samostatném souboru
ale můžeš si formulář udělat jak chceš.
Samostatný formulář v UI\Control komponentě si můžeš udělat třeba takhle:
final class TestForm extends UI\Control
{
public function __construct(private TestModel $testModel){} // di
public function create(?string $action, ?int $id): self
{
$form = new Form();
.......
$form->addSubmit('send');
match($action){
'create' => $form->setDefaultValues = $this->testModel->getDefaultOne(),
'update' => $form->setDefaultValues = $this->testModel->getOne($id),
}
$form->onSuccess[] = fn() => $this->processSuccess($form, $action, $id);
$this->addComponent($form, 'form'); // interní formulář - editováno
return $this;
}
private function processSuccess(Form $form, ?string $action, ?int $id): void
{
match($action){
'create' => $this->testModel->createOne($form->getValues()),
'update' => $this->testModel->updateOne($form->getValues()),
}
$this->getPresenter()->redrawControl('testTable');
}
}
a použít v presenteru třeba takhle:
final class TestPresenter extends UI\Presenter
{
public function __construct(private TestForm $testForm){} // di
public function createComponentTestForm(): TestForm
{
return $this->testForm->create(action: $this->subAction, id: $subId);
}
}
Komponenta TestForm přebírá modelovou třídu pomocí di a předává se do presenteru pomocí di a musí tak být registrována jako služba. Takto to jde, pokud se v akci presenteru použije komponenta jednou, což je skoro vždycky. Pokud by se komponenta měla použít opakovaně, je vhodné použít pro tvorbu komponenty factory, která zajistí opakované vytváření nových instancí komponenty (Multiplier).
Formulář přebírá z presenteru nejen $id záznamu, ale i mód editace create/update a mírně modifikuje svoji činnost podle tohoto módu.
Presenter se stará o:
- vykreslení šablony akce
- překreslení snippetu s komponentou při ajaxovém requestu
- převzetí parametrů pro řízení komponenty $subAction + $subId
- tvorbu komponenty (bez factory nebo s factory)
- předání parametrů pro komponentu
Komponenta si interakci formuláře řeší sama a volá presenter jenom pro překreslení snippetu v šabloně akce – pomocí getPresenter().
Nejsou potřeba ani eventy ani signály a většinou ani ne factory. Řekl bych, že jsou kompetence mezi presenterem a komponentou takto rozděleny celkem ideálně a projekty jsou přehlednější.
Komunikace s grafem
Záleží jak tu komunikaci chceš postavit a jaká data se zobrazují v tabulce, jaká v info okně a jaká v grafu. Můžeš zobrazit po kliknutí na řádek současně info okno i graf a po editaci v info okně je překreslit, nebo můžeš zobrazovat samostatně info okno a samostatně graf kliknutím na různé odkazy v řádku tabulky. Výše uvedená koncepce routování $subction + $subId to snadno umožňuje.
Nezávislé ovládání info x graf
{snippet 'testTable'}
{foreach $rows as $id => $row}
<tr>
<td>{$row->title}</td>
<td>
<a n:href="'this', subAction: 'info', subId: $id" class="ajax">info</a>
</td>
<td>
<a n:href="'this', subAction: 'graph', subId: $id" class="ajax">graf</a>
</td>
</tr>
{/foreach}
{/snippet}
+
{snippet 'infoPanel'}
{if $subAction === 'info'}
{control 'infoControl'}
{/if}
{/snippet}
{snippet 'graphPanel'}
{if $subAction === 'graph'}
{control 'graphControl'}
{/if}
{/snippet}
Pokud se mají info a graf zobrazovat současně a zobrazují stejná data jenom jiným způsobem, je docela možné mít na to jednu komponentu a info a graf spojit dohromady v šabloně.
Editoval m.brecher (1. 8. 23:24)
- Kamil Valenta
- Člen | 783
m.brecher napsal(a):
{snippet 'infoPanel'} {if $subAction === 'info'} {control 'infoControl'} {/if} {/snippet} {snippet 'graphPanel'} {if $subAction === 'graph'} {control 'graphControl'} {/if} {/snippet}
Možná jsem se v délce příspěvku ztratil, ale mám pocit, že tady infoControl a graphControl spolu nijak nekomunikuje. Vlákno není o překreslování dvou snippetů v šabloně na základě parametru z routy.
Eventy, do kterých rodič předá potřebné invalidace, jsou nejlepší cesta.
- m.brecher
- Generous Backer | 814
@KamilValenta
Možná jsem se v délce příspěvku ztratil, ale mám pocit, že tady infoControl a graphControl spolu nijak nekomunikuje. Vlákno není o překreslování dvou snippetů v šabloně na základě parametru z routy.
Téma vlákna jsou presenter + tabulka + info okno + graf, kde se požaduje komunikace mezi komponentami, mimo jiné i mezi info oknem a grafem toho typu, kdy v info okně něco naklikám a změny uvidím v grafu. Pochopil jsem to tak, že mám v grafu nějaké readonly data, která mohu v info okně editovat. Uložení editovaných dat v info okně a následné promítnutí změn do grafu je jednosměrná komunikace prostřednictvím databáze + překreslení snippetu a nejjednodušší cestou jsou klasické Nette ajax snippety.
Ano kód který Jsi zmínil mírně odbočil od tématu, v příspěvku není totiž zadáno jak se mají info x graf ovládat (zapínat/vypínat) – můžu je zapínat nezávisle samostatnými odkazy s tím, že požadovaná promítání změn tam funguje také, jenom je potřeba doplnit do presenteru/komponenty příslušná překreslení snippetů – už jsem i tak obsáhlý příspěvek nechtěl natahovat.
Eventy, do kterých rodič předá potřebné invalidace, jsou nejlepší cesta.
Jasně eventy jsou také cesta – tato cesta už byla ve vlákně zmíněna. Jednoduché překreslení snippetů řízené parametrem presenteru zadaný problém řeší elegantně, takže máme minimálně dvě použitelné cesty na výběr.
Editoval m.brecher (1. 8. 12:33)
- Taco
- Člen | 50
Zásady:
1/ Presenter je taky komponenta.
2/ Komponenta by měla znát své přímé podkomponenty, ale už ne
pod-podkomponenty.
3/ Komponenta by neměla znát svého rodiče.
Řeším to tedy podobně jako máš bod 4. Komponentě předávám callback, ve kterém je obsluha toho, co se má stát když se komponenta dostane do nějakého stavu. Tím komponenta nezná svého rodiče, a rodič může reagovat a předat informaci sousední komponentě.
- Fires
- Člen | 96
Díky moc všem. Jdu tedy cestou 4. Presenter zná své komponenty a ví jak je připojit. Komponenty netuší co je nad nima ale ví funkci kterou mají vykonat když se něco stane. Napsal jsem tak už pár komponent v současném projektu vč. zanořených komponent a je to přehledně a funguje. Myslím že tohle vlákno pomůže hodně lidem díky ještě jednou.