neobnoví se snippet v komponentě

trta911
Člen | 35
+
0
-

Mám komponentu kterou mám umístěnou v šabloně:

<div class="row">
    {snippetArea controlWrapper}
        {control videoWizard}
    {/snippetArea}
</div>

V komponentě mám nějakou základní šablonu do které includuju další sub-šablonu:

{snippetArea stepWrapper}
        {include "step".$curentStep.".latte"}
{/snippetArea}

ta sub-šablona kromě jiného má v sobě

{snippet uploadedVideo}
    <div class="row">
        <div class="col-12">
        {if !empty($items)}
           .....
        {/if}
        </div>
    </div>
{/snippet}

v rámci komponenty po uploadu volám

naja.makeRequest('GET', {plink videoWizard:refreshVideo!}, {});

v komponentě mám

class AddVideoWizardControl extends Control
{
...
    public function handleRefreshVideo() : void
    {
            ...
            $this->getPresenter()->redrawControl("controlWrapper");
            $this->redrawControl("stepWrapper");
            $this->redrawControl("uploadedVideo");
    }
...
}

ajax request se vykoná v pohodě, ale nepřekreslí se lautr nic. (v network tabu chromu vidím akorát {„state“: []}).

Kde prosím dělám chybu?

Editoval trta911 (19. 2. 2023 22:09)

m.brecher
Generous Backer | 863
+
+1
-

@trta911

ajax request se vykoná v pohodě, ale nepřekreslí se lautr nic.

Ahoj, na 90% si myslím, že chybu děláš v tom, že v handle metodě komponenty voláš zbytečně snippetArea, která obaluje vykreslení komponenty. Komponenta z třídy UI/Control dle dokumentace Nette „si umí zapamatovat, jestli při signálu došlo ke změnám, které si vyžadují jej překreslit“, takže ji v šabloně obalovat do snippetArea je asi zbytečné.

Udělal jsem malý zjednodušený test podobný Tvému kódu:

class Test extends Control
{
    public int $num = 1;

    public function handleIncrement()
    {
        $this->num ++;
        $this->presenter->redrawControl('controlWrapper');  // tohle se mě zdá zbytečné
        $this->redrawControl('numWrapper');
    }

    public function render()
    {
        $this->template->num = $this->num;
        $this->template->render(......);
    }
}

šablona akce presenteru:

    {snippetArea 'controlWrapper'}
        {control 'test'}
    {/snippetArea}

šablona komponenty:

<p><a n:href="increment!" class="ajax">Exec!</a></p>

{snippet 'numWrapper'}
<p>num: {$num}</p>
{/snippet}

Výsledek: Ajaxový request se pošle, ale šablona se NEPŘEKRESLÍ.

Vyřadil jsem dle mého názoru zbytečný snippetArea okolo komponenty:

class Test extends Control
{
    public int $num = 1;

    public function handleIncrement()
    {
        $this->num ++;
//        $this->presenter->redrawControl('controlWrapper');
        $this->redrawControl('numWrapper');
    }

    public function render()
    {
        $this->template->num = $this->num;
        $this->template->render(......);
    }
}
{*    {snippetArea 'controlWrapper'}*}
        {control 'test'}
{*    {/snippetArea}*}

A takhle upravené už to funguje – Ajaxový request se pošle a šablona se PŘEKRESLUJE !

Editoval m.brecher (20. 2. 2023 2:06)

trta911
Člen | 35
+
0
-

@m.brecher

moc děkuji, tohle mě funguje, ale ne v té includované šabloně kde to potřebuji obnovit

basic.latte

{snippetArea stepWrapper}
        {include "step".$curentStep.".latte"}
{/snippetArea}

step1.latte

{snippet uploadedVideo}
    <div class="row">
        <div class="col-12">
        {if !empty($items)}
           .....
        {/if}
        </div>
    </div>
{/snippet}

tam jsem to zkoušel se snippety, bez snippetů, se snippet areou a nedokázal jsem to přemluvit. Pokud ale snippet přesunu z té zanořené šablony a po tvé radě.

m.brecher
Generous Backer | 863
+
+1
-

@trta911

tam jsem to zkoušel se snippety, bez snippetů, se snippet areou a nedokázal jsem to přemluvit

OK, zkusil jsem tedy ještě vložit do šablony komponenty další šablonu – zde se již musí dle dokumentace Nette použít {snippetArea} a invalidovat nejprve snippetArea a potom vnořený snippet https://doc.nette.org/…ication/ajax#….

class Test extends Control
{
    public int $num = 1;

    public function handleIncrement()
    {
        $this->num ++;
        $this->redrawControl('wrapper');
        $this->redrawControl('increment');
    }

    public function render()
    {
        $this->template->num = $this->num;
        $this->template->render(__DIR__.'/test.latte');
    }
}

šablona akce

{control 'test'}

šablona komponenty test.latte

<p><strong>Test snippetu</strong></p>

{snippetArea 'wrapper'}
    {include 'increment.latte'}
{/snippetArea}

vnořená šablona increment.latte

<p><a n:href="increment!" class="ajax">Inkrementuj</a></p>

{snippet 'increment'}
    <p>num: {$num}</p>
{/snippet}

Vyzkoušeno a překreslování funguje.

Takže můžeš vyjít z toho, že takhle použité snippetArea a snippet funguje – v souladu s dokumentací. Chybu budeš mít někde jinde – dodání dat do šablon, nebo javascriptový request, …?

trta911
Člen | 35
+
0
-

@m.brecher

mockrát díky! Tak jsem díky tomu zjistil, že se mi v tom handlu (jinak to bylo právě v pohodě tak jsem ani nepojal podezření) ne úplně dobře předávala ta nejdůležitější reference (která nastavuje tu includovanou sub-šablonu) a díky tomu se to nevykreslovalo. Teď už je to ok.

MikKuba
Člen | 83
+
0
-

@mbrecher

Narazil jsem na toto vlákno při řešení myslím velmi obdobného problému.

šablona presenteru

{block content}
	{snippet wrapper}
		{control detailView}
	{/snippet}
{/block}

{block scripts}
	<script>
		$(document).ready(function() {
			function updateSnippet() {
				$.ajax({
					url: '?do=actualizeOrder',
					method: 'POST',

				});
			}

			// Zavolání funkce hned po načtení stránky
			updateSnippet();

			// Opakování každých 5 sekund
			setInterval(updateSnippet, 5000);
		});
	</script>
{/block}

Presenter

public $shop = null;
public $order = null;

    public function actionDetail($url, $code)
    {
        $this->shop = $this->userFacade->findOneBy(['url' => $url]);
        if(!$this->shop){
            $this->error('Obchod nebyl nalezen');
        }
        $this->order = $this->orderFacade->findOneBy(['orderCode' => $code]);
        if(!$this->order){
            $this->error('Objednávka nebyla nalezena');
        }
    }

    protected function createComponentDetailView()
    {
        return $this->detailViewFactory->create($this->shop, $this->order);
    }

    public function handleActualizeOrder()
    {
        $this->order = $this->orderFacade->findOneBy(['orderCode' => $this->order->getOrderCode()]);

        if ($this->isAjax()) {
            $this['detailView']->actualize($this->order);
            $this->redrawControl('wrapper');
            $this->redrawControl('orderDetailView');
        }
    }

A samotná komponenta, která v šabloně má:

{snippet orderDetailView}

Aktuální stav:
{if !$order->isPreparation() && !$order->isDone()}
    <span class="badge badge-warning">Přijato</span>
{elseif $order->isPreparation() && !$order->isDone()}
     <span class="badge badge-info">Připravuje se</span>
{elseif $order->isDone()}
     <span class="badge badge-success">Hotovo</span>
{/if}

a komponenta jako třída:

class DetailViewFactory extends Control
{

    public $onError = [];

    public $onSuccess = [];

    private $user;
    private Orders $order;

    private $orderData;

    public function __construct(
        User $user,
        Orders $order,
        private OrderFacade $orderFacade,
        private Request $request,
    )
    {
        $this->user = $user;
        $this->order = $order;
        $this->orderData = $this->order;
    }


    public function render() {
        $this->template->setFile(__DIR__ . '/../templates/Detail/view.latte');
        $this->template->userData = $this->user;
        $this->template->order = $this->orderData;
        $this->template->render();
    }

    public function actualize(Orders $order)
    {
        $this->orderData = $order;
    }

    public function getData(){
        return $this->orderFacade->findOneBy(['orderCode' => $this->order->getOrderCode()]);
    }
}

interface IDetailViewFactory
{
	/**
     * @param User $user
     * @param Orders $order
	 * @return \App\PublicModule\Components\DetailViewFactory
	 */
	public function create(User $user, Orders $order): DetailViewFactory;
}

vidím, že se příkaz pomocí ajaxu provede a vrátí se snippet s aktualizovaným obsahem, ale reálně se mi na stránce nic nepřekreslí.
Když jsem zkoušel cvičně do komponenty přesunout handle metodu a v komponentě šabloně dát tlačítko provádějící ajaxový handle komponenty, tak to fungovalo. Ale za nic mi to nejde z toho skriptu v presenteru, aby se překreslil obsah komponenty.
Dělám to takto proto, že v šabloně presenteru mohu definovat blok do {scripts} což se vypíše do layoutu pod ostatní skripty, kde mj. inicializuju jquery a ajax.

v případě nouze můžu do komponenty dát tlačítko neviditelné a to skriptem z šablony presenteru volat a „klikat“ na něj, ale přijde mi to jako nevhodný způsob

m.brecher
Generous Backer | 863
+
0
-

@MikKuba

Nette Ajax snippety dokáží hladce překreslovat po aktualizaci v databázi a je zbytečné to řešit pomocí jQuery nekonečným cyklem každých 5 sekund.

Uživatel klikne na nějaké tlačítko v komponentě a ajaxem se provede update stavu objednávky v databázi, následně jenom překreslíme šablonu objednávky.

Co v kódu nevidím:

  • kde jsou odkazy/formulář v šabloně objednávky, kde se odesílá request pro aktualizaci objednávky ?
  • kde přesně se provádí aktualizace objednávky v modelu

Komponenta DetailViewFactory

  • získá v konstruktoru datový objekt Orders (z presenteru), který uloží do dvou propert $order + $orderData
  • v metodě render() předá $orderData do šablony – to je OK
  • v metodě actualize() která se volá ve zpracování signálu handleActualizeOrder() získá do stejné property z presenteru zase $orderData – to je divné

Dle mého názoru je struktura presenteru a komponenty špatně postavená. Metoda handleActualizeOrder() bych řekl, že je zbytečná, úplně bych ji smazal, stejně jako pomocné skripty jQuery, které ji volají.

Pojďme najít v kódu presenteru – pošli KOMPLETNÍ kód presenteru!) místo, kde se obsluhuje request na změnu v objednávce + najděme místo, kde uživatel na něco kliká, čím se mění stav objednávky – to bude nějaký formulář, nebo nějaké GET odkazy – pošli kompletní kód šablony komponenty a jestli tam je nějaký formulář tak celý formulář!. Pak už vymyslíme nějaké jednoduché řešení překreslení.

Poznámka: Interface IDetailViewFactory by měl v návratovém typu vracet DetailView komponentu, protože je divné, když se factory i komponenta jmenují stejně. Komponenta by neměla mít suffix Factory. Ono to funguje, ale je to v rozporu s best practice Nette. Všimni si – metoda createComponentDetailView() by měla vracet komponentu DetailView, ale vrací DetailViewFactory.

Oprava Interface:

interface IDetailViewFactory
{
   // anotace jsou zbytečné - máš otypované parametry i return type - anotace smazat!
	public function create(User $user, Orders $order): DetailView;  // změna return type
}

Oprava komponenty:

class DetailView extends Control   // přejmenujeme
{

    public $onError = [];

    public $onSuccess = [];

    private $user;
    private Orders $order;

    private $orderData;

    public function __construct(
        User $user,
        Orders $order,
        private OrderFacade $orderFacade,
        private Request $request,
    )
    {
        $this->user = $user;
        $this->order = $order;
        $this->orderData = $this->order;
    }


    public function render() {
        $this->template->setFile(__DIR__ . '/../templates/Detail/view.latte');
        $this->template->userData = $this->user;
        $this->template->order = $this->orderData;
        $this->template->render();
    }

   // smazat
   // public function actualize(Orders $order)
   // {
   //     $this->orderData = $order;
  //  }

    // metoda getData() je nadbytečná - tato data předává komponentě presenter v konstruktoru - smazat !!
    // public function getData(){
   //     return $this->orderFacade->findOneBy(['orderCode' => $this->order->getOrderCode()]);
   // }
}

oprava presenteru:

public $shop = null;
public $order = null;

    public function actionDetail($url, $code)
    {
        $this->shop = $this->userFacade->findOneBy(['url' => $url]);
        if(!$this->shop){
            $this->error('Obchod nebyl nalezen');
        }
        $this->order = $this->orderFacade->findOneBy(['orderCode' => $code]);
        if(!$this->order){
            $this->error('Objednávka nebyla nalezena');
        }
    }

    protected function createComponentDetailView()
    {
        return $this->detailViewFactory->create($this->shop, $this->order);
    }

// smazat - je bezpochyby zbytečná
//   public function handleActualizeOrder()
//    {
//        $this->order = $this->orderFacade->findOneBy(['orderCode' => $this->order->getOrderCode()]);
//
//        if ($this->isAjax()) {
//            $this['detailView']->actualize($this->order);
//            $this->redrawControl('wrapper');
//            $this->redrawControl('orderDetailView');
//        }
//    }

Pošli ty kompletní kódy a najdeme kam umístit invalidaci snippetů !

Editoval m.brecher (5. 8. 4:12)

MikKuba
Člen | 83
+
0
-

@mbrecher

Nevím jestli zbytek presenteru pomůže, protože přepínání stavu objednávky neprobíhá na detailu objednávky. To je stránka, kterou vidí uživatel co vytvoří objednávky a dostane se na detail, kde je i stav.

Samotný stav pak přepíná admin v přehledu, tedy že ji připravuje a pak je hotovo (myšleno pro objednávky do podniku, kde si někdo objedná občerstvení a trochu live vidí, když to někdo z obsluhy odklikne jakože už to chystá, a pak že je hotovo).

Proto tam je to jquery, které ajaxem získává cyklicky stav dané objednávky.
Ke změně stavu dochází v jiném presenteru, dokonce v jiném modulu. Je i toto řešitelné tím snippetem? Protože o takové variantě jsem neslyšel ještě, že by dokázal načtený nette snippet si sám hlídat stav v databázi a překreslovat se.

Editoval MikKuba (5. 8. 11:22)

m.brecher
Generous Backer | 863
+
0
-

@MikKuba

Samotný stav pak přepíná admin v přehledu

Aha, potom je to úplně jinak než jsem z neúplného popisu pochopil. Změna stavu objednávky se provádí v jiném prohlížeči a v presenteru který Jsi poslal se cyklicky javascriptem vyvolává překreslení komponenty, aby zobrazovala aktuální stav. Nelze vyvolat překreslení detail komponenty v okamžiku, kdy se provádí vlastní update v databázi a použití metody handleAktualizeOrder() spouštěné cyklicky signálem z javascriptu je dobrá cesta.

Nicméně i tak platí, že kód presenteru a komponenty není dobře sestaven. Budu si to muset pořádně rozmyslet a pak pošlu navrženou opravu.

Uděláme to jak to máš teď, ale sestavíme kód presenteru a komponenty nějak logicky. Javascript bude ajaxem spouštět handle v presenteru, tím se spouští současně celá akce ale na výstup se pošlou jenom invalidované snippety. Je zbytečné invalidovat snippet ‚wrapper‘ a současně i komponentu.

Editoval m.brecher (8. 8. 5:39)

mskocik
Člen | 61
+
+1
-

@MikKuba ty by si mal volať aj prekreslenie snippetu v tej komponente – ci uz v tej handle metode, alebo v metode $component->actualizeOrder():

// presenter.php
    public function handleActualizeOrder()
    {
        $this->order = $this->orderFacade->findOneBy(['orderCode' => $this->order->getOrderCode()]);

        if ($this->isAjax()) {
            $this['detailView']->actualize($this->order);
            $this['detailView']->redrawControl('orderDetailView');  // presenter NEMA v sablone snippet 'orderDetailView'
            $this->redrawControl('wrapper');	// ak v tomto snippete nemas nic okrem danej komponenty, mozes vynechat
        }
    }

EDIT: pridany komentar k redrawControl('wrapper')

Editoval mskocik (5. 8. 20:52)

m.brecher
Generous Backer | 863
+
0
-

@MikKuba

Tady jsem něco objevil – určitě není dobře kód překreslující snippety v metodě presenteru handleActualizeOrder():

public function handleActualizeOrder()
    {
        $this->order = $this->orderFacade->findOneBy(['orderCode' => $this->order->getOrderCode()]);

        if ($this->isAjax()) {
            $this['detailView']->actualize($this->order);
            $this->redrawControl('wrapper');		// to je OK
            $this->redrawControl('orderDetailView');  // to je špatně
        }
    }

Nelze překreslovat snippet komponenty ‘orderDetailView’ z presenteru, z presenteru můžeš překreslit snippet šablony presenteru, nebo CELOU komponentu voláním $this->getComponent(‘myComponent’)->redrawControl().

Není ale potřeba překreslovat současně obalující snippet a současně komponentu, mělo by stačit překreslit jen obalující snippet. Zkus řádek $this->redrawControl(‘orderDetailView’); odstranit a uvidíš. Pravděpodobně tam bude ještě jiná chyba, ale tohle bychom měli opravit.

m.brecher
Generous Backer | 863
+
0
-

@MikKuba

Všiml si toho i @mskocik a navrhuje trochu jiný postup než já – překreslit komponentu a sice konkrétní snippet v komponentě. Je to asi jedno, ale jde i překreslit celou komponentu tím, že se zavolá redrawControl() bez parametrů a potom se překreslují VŠECHNY snippety komponenty.

a) mělo by stačit buďto překreslit wrapper, nebo komponentu, ne obojí
b) je zbytečná komplikace posílat do komponenty data objednávky metodou actualize(), protože se tam pošlou konstruktorem v akci

Zkus opravit kód tímto způsobem:

public function handleActualizeOrder()
    {
    //    $this->order = $this->orderFacade->findOneBy(['orderCode' => $this->order->getOrderCode()]);

        if ($this->isAjax()) {
       //     $this['detailView']->actualize($this->order);
       //     $this->redrawControl('wrapper');
       //     $this->redrawControl('orderDetailView');
              $this['detailView']->redrawControl();
        }
    }

Možná tam bude ještě jiná chyba, ale uvidíme.

Editoval m.brecher (5. 8. 14:24)

m.brecher
Generous Backer | 863
+
+1
-

@MikKuba

Tak to mám vyřešené – napsal jsem to znovu s použitím ajax knihovny Naja. Zde je testovací kód:

Presenter

class OrderPresenter extends UI\Presenter
{
    public function __construct(private OrderControl $orderControl)  // komponentu jako službu
    {}

    public function createComponentOrderControl(): OrderControl
    {
        return $this->orderControl->setId(1);  // 1 je testovací $id
    }

    public function handleRefreshOrder(): void
    {
        $this['orderControl']->redrawControl();  // překreslení komponenty OrderControl
    }

}

šablona presenteru default.latte – blok s komponentou

{block 'content'}
    <nav>
        <a n:href="this">reload</a>
        <a n:href="refreshOrder!" class="ajax">updateOrder</a>  {* testovací odkazy *}
    </nav>

    {control 'orderControl'}

{/block}

šablona presenteru default.latte – blok s javascriptem – ajaxový endless loop

{block 'head'}
<script src="https://unpkg.com/naja@3/dist/Naja.min.js"></script>  // Naja

<script>
    class Refresher  // třída pro nekonečný refresh - signálem refreshOrder
    {
        #url
        #timeMs

        constructor(url, timeMs)
        {
            this.#url = url
            this.#timeMs = timeMs
        }

        init()
        {
            this.#wait(this.#timeMs)
                .then(() => {
                    naja.makeRequest('GET', this.#url)
                    this.init()   // recursive call - endless loop
                })
        }

        #wait(ms)
        {
            return new Promise((resolve) => setTimeout(resolve, ms))
        }

    }

    refresher = new Refresher({$presenter->link('refreshOrder!')}, 700)
    document.addEventListener('DOMContentLoaded', () => {
        refresher.init()
        naja.initialize()
    })
</script>

{/block}

Komponenta:

class OrderControl extends Control
{
    private int $id;

    public function __construct(private OrderModel $orderModel)  // model jako služba
    {}

    public function setId(int $id): self   // id z presenteru
    {
        $this->id = $id;
        return $this;
    }

    public function render(): void
    {
        $this->template->order = $this->orderModel->getOne($this->id);
        $this->template->render(__DIR__.'/orderControl.latte');
    }

Model pro testování

class OrderModel
{
    public function __construct(private Explorer $explorer)
    {}

    public function getOne(int $id): ?ActiveRow
    {
        return $this->explorer->table('order')->get($id);
    }
}

testovcí šablona komponenty orderControl.latte

{snippet 'control'}
    <p>objednávka: {$order->title}</p>
{/snippet}

Je to otestované a funguje to. Překreslení je úplně jednoduché. Překreslení se vyvolává signálem z javascriptu a volá se handleRefreshOrder() v presenteru – ten pouze invaliduje celou komponentu.

Jak to funguje ?

Javascript spustí ajaxem handle v presenteru – ten pouze invaliduje komponentu – aby se později překreslil její snippet.

Spouští se i akce presenteru, ta zde ale nedělá nic, pouze vykreslí šablonu default.latte. Kdyby ale v presenteru byla metoda akce, tak se spustí.

Šablona default.latte, která vykresluje komponentu – předá požadavek na vytvoření komponenty do presenteru.

Presenter volá createComponentOrderControl() , ten vytvoří komponentu (já mám komponentu jako službu, ale můžeš si ji vytvořit i přes factory).

Komponenta převezme settrem z presenteru $id, konstruktorem z di model a pošle čerstvá data do šablony.

Protože je request ajaxový a protože je komponenta invalidována, odešle Nette snippet komponenty s čerstvými daty.

Funguje to a není to složité.

Editoval m.brecher (8. 8. 5:40)

MikKuba
Člen | 83
+
0
-

@mbrecher

Děkuju moc za obsáhlou odpověď, funguje to opravdu dobře! A děkuju za postřeh @mskocik !
Hlavně mi přišlo důležitý odstranit ten nešvar, že v komponentě mám neviditelný button pro aktualizaci, který ale volám ze scriptu v presenterové šabloně, protože tam jsem schopen vložit block pro skripty do příslušné části layoutu, pod jquery a další.

A díky tomu pak i dobře funguje to cyklické volání handlu, bez nutnosti mít někde skrytá tlačítka.