Komunikace mezi komponentami již naposled

Fires
Člen | 96
+
0
-

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):

  1. 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.
  2. 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);
}
  1. „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ý)
  2. 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
+
+1
-

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 :)

Felix
Nette Core | 1193
+
+1
-

Definice eventu pres komponenty mi prijde nejvhodnejsi. Pouzivam to tak na projektech a nemel jsem s tim trable.

Kcko
Člen | 467
+
+1
-

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

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

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í.

Fires
Člen | 96
+
0
-

Moc díky všem. Primárně tedy budu užívat metodu číslo 4. Příjde mi to jako nejpřehlednější způsob.

Pavel Kravčík
Člen | 1191
+
+1
-

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

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

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

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

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

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.