Komponenta v komponentě, render error

Damo
Člen | 69
+
0
-

Mám komponenty, které když volám přímo v latte u presenteru, všechno mi funguje.
Jde nette + naja.

Ale jakmile dam komponentu do latte jiné komponenty, pak nastanou problémy při volání handle.

Handle probublá až do vnořené komponenty, ale poté se to vratí na parent komponentu do renderu, a tam to zhavaruje.

Komponenta vnorena

    public function render(?FilesExplorerPayload $filesExplorerPayload = null): void {
        if (!$filesExplorerPayload) {
            // Tohle je tady aby se to preskocilo, protoze ahndle nize zavola i tento render a parmater neni k dispozici, takze to necheme vykreslovat
            $this->template->render();
            return;
        }

        $this->template->setFile(dirname(__FILE__) . "/FilesExplorerComponent.latte");

        $this->template->unique = uniqid();
        $this->template->componentName = $filesExplorerPayload->getComponentName();
        $this->template->className = $filesExplorerPayload->getClassName();
        $this->template->filesExplorer = $filesExplorerPayload;

        $this->template->render();
    }
...
...
    public function handleAddCollection(?int $parentCollectionId = null): void {
        $this->template->setFile(dirname(__FILE__) . "/templates/editCollection.latte");
        $this->template->editCollectionForm = new EditCollectionFormPhoenix($parentCollectionId);
        $this->redrawControl("mcModal");
    }

Základní koponenta, ve které používám tu „vnorenou“

    public function render(?array $options = []): void {
        if (!$options) {
            $this->template->render(); // error
            return;
        }

        $this->template->setFile(dirname(__FILE__) . "/SelectImageComponent.latte");
        $this->template->unique = uniqid();
        $this->template->options = $options;
        $this->template->render();
    }
...
...
    // Tenhle modal v latte vola tu vnorenou komponentu vyse
    public function handleOpenDialog(): void {
        $data = FilesLevel::getFilesTemplateData();
        $this->template->setFile(dirname(__FILE__) . "/openDialog/openDialog.latte");
        $this->template->treeview = $data[0];
        $this->template->filesExplorer = $data[1];
        $this->template->filesBrowser = new FilesBrowserPayload();
        $this->redrawControl('mcCanvas');
    }

Cely to skonci na erroru
Latte\Engine::render(): Argument #1 ($name) must be of type string, null given,
na radku
$this->template->render();

Kdyz zkousim ve vnorene komponente u handleu rovnou dat na konec
$this->template->render();
Tak to funguje, ale jen kdyz je komponenta uvnitr jine. Pokud ji pouziji nekde v presenteru, pak uz nefunguje

Prosim o radu, co je spatne, jak to udelat aby se vyrendroval handle u vnorene komponenty?

Díky

Editoval Damo (18. 10. 14:59)

Damo
Člen | 69
+
0
-

Vyresil jsem to kdyz vlozim do renderu jeste tuto podminku, a hodim tam dummy.latte (prazdnou sablonu).
Ale pokud to jde elegantneji, budu rád, když mi někdo poradí.

        if (!$this->template->getFile()) {
            $this->template->setFile(__DIR__ . '/dummy.latte');
        }

Editoval Damo (18. 10. 18:37)

nightfish
Člen | 528
+
+3
-

@Damo
Pokud chceš renderovat šablonu, je potřeba explicitně nastavit soubor se šablonou (setFile(...)).

V kódu

 if (!$options) {
            $this->template->render(); // error
            return;
        }

žádné volání setFile() nemáš. Není mi moc jasné, co by se mělo renderovat. Pokud nic, tak prostě ten řádek s $this->template->render(); smaž. A stejný problém máš i v render metodě vnořené komponenty.

A druhá věc – je best practice nepoužívat v komponentách parametry render() metod, protože při AJAXovém volání se parametry render() metod nepředávají. Ideální je si všechny parametry předat přes konstruktor (nebo přinejhorším setter) a v šabloně mít jenom {control nazevKomponenty}.

Damo
Člen | 69
+
0
-

Prvně se mi přes render vykresli komponenta, a potom přes handle si otevírám třeba modal, a tam už nepotřebuji rendrovat znovu tu komponentu, proto v tom renderu jsou ty podminky. Odebráním $this->template->render(); se nevyrendruje ani ten handle, return payload je prázdný. Při těch komponentách v komponentě, pomůže ta dummy. Já tam ty parametery když volám handle nepotřenbuji, pač s komponentou nijak nehybam, nechci ji ani rendrovat, jde mi jen o render z handle. Možná kdyby byl nějaký větší celek kódu jak komponenta vypadá, byl bych rád. V doku jsou jen drobné příklady. Díky

m.brecher
Generous Backer | 911
+
+1
-

@Damo

Odebráním $this->template->render(); se nevyrendruje ani ten handle

Kód který Jsi poslal je jenom výsek a nejsou vidět všechny souvislosti, pokud řádek $this->template->render() něco skutečně vykresluje, potom se někde jinde v kódu, který už nevidíme, nastavuje nějaká šablona setFile(), která se ale nenastavuje vždycky a proto to Latte v jiné situaci vyhazuje tu chybu, kterou lze nouzově workaroundem vyřešit pomocí dummy.latte, máš ale jednoznačně špatné logické řízení.

Když to spočítám, máš tam celkem dvě komponenty v ajax režimu s celkem 4mi různými šablonami, které se renderují asynchronně v render() nebo handle() to už je docela složité a náročné na udržení logické kontroly. Co by se dalo poradit – čistší řešení než dummy.latte je mít logické řízení pomoví stavových proměnných tak, aby se renderování neexistující šablony vůbec nevolalo, ale volalo se tehdy, kdy potřeba je.

Pomohlo by, kdyby Jsi podrobně popsal funkci těch komponent, dodal komplet kód – komponenty, šablony, akci presenteru a pak by se dalo najít, kde je chyba, ale asi bude v absenci jasného logického řízení stavu.

Damo
Člen | 69
+
0
-

Tak jsem predelal tovarnicku, aby mi data chodila pres construct

<?php

declare(strict_types=1);

namespace App\Components\Phoenix\SelectImage;

use App\AdminModule\Models\PhoenixModels\FilesBrowser\Entities\FilesBrowserPayload;
use App\Components\BaseComponent;
use App\Models\BaseComponentPayload;

class SelectImageComponent extends BaseComponent {
    private ?BaseComponentPayload $baseComponentPayload = null;
    public function __construct(?BaseComponentPayload $baseComponentPayload) {
        $this->baseComponentPayload = $baseComponentPayload;
    }

    public function render(): void {
        if (!$this->baseComponentPayload) {
            if (!$this->template->getFile()) {
                $this->template->setFile(__DIR__ . '/dummy.latte');
            }
            $this->template->render();
            return;
        }
        $this->template->setFile(dirname(__FILE__) . "/SelectImageComponent.latte");
        $this->template->unique = uniqid();
        $this->template->componentName = $this->baseComponentPayload->getComponentName();
        $this->template->className = $this->baseComponentPayload->getClassName();
        $this->template->render();
    }

    public function handleOpenDialog(): void {
        $filesBrowserPayload = new FilesBrowserPayload('selectImage');
        $filesBrowserPayload->setAsDialog();

        $this->template->setFile(dirname(__FILE__) . "/openDialog/openDialog.latte");
        $this->template->filesBrowser = $filesBrowserPayload;
        $this->redrawControl('mcCanvas');
    }
}

Problém je, že jsem se tech podmínek v render nezbavil. S těmi podmínakmi to běží výborně, bez podmínek to popadá.

Možná to nevysvětluji dobře.

První vykreslení komponenty proběhne v pořádku, render zafunguje a vyrendruje šablonu SelectImageComponent.latte.
V šabloně, volám handle openDialog, a tam je kámen úrazu. Já nechci už s komponenentou nic dělat, ten openDialog, pouze vymeni empty snippet pro modal mcCanvas.

SelectImageComponent.latte

<div
        class="{$className}"
        id="selectImage{$unique}"
        data-component="{$componentName}"
>
    <a n:href="openDialog!" class="ajax no-save-protect btn btn-phoenix-secondary w-100 h-100 d-flex flex-column justify-content-center align-items-center">
        <i class="bi bi-plus-circle mb-2 fs-6"></i>
        <span>Select image</span>
    </a>
</div>
{snippet mcCanvas}{/snippet}

obsah snippetu je ovsem ulozen v sablone openDialog.latte

{snippet mcCanvas}
    <div class="offcanvas offcanvas-end offcanvas-100 bg-body no-save-protect" id="offcanvasOpenDialogFiles" tabindex="-1" aria-labelledby="offcanvasRightLabel">
        <div class="offcanvas-header modal-header">
            <h5 id="offcanvasRightLabel" class="modal-title">Select file</h5><button class="btn-close text-reset" type="button" data-bs-dismiss="offcanvas" aria-label="Close"></button>
        </div>
        <div class="offcanvas-body" style="--phoenix-navbar-top-height: -1rem">
            {control phoenixFilesBrowser $filesBrowser}
        </div>
        <div class="offcanvas-footer modal-footer">
            <button class="btn btn-primary m-2" type="button" data-bs-dismiss="offcanvas">
                Use
            </button>
        </div>
    </div>
    <script>
        $(function() {
            const $offcanvas = $("#offcanvasOpenDialogFiles").mcOffcanvasService();
        });
    </script>
{/snippet}

takze ze zavolaneho handle vlákno kodu pak přeskočí na render() metodu. Ale data pro render() jaksi nemám k dispozici, protoze uz jsou davno pryč. Prot je tam ta podmínka na $this->baseComponentPayload.
Ta druhá podmínak je tam proto, protože v tom modalu je další kompoenta, která zase vola dalsí handle, a z toho handle vlákno kodu hierarchicky se vraci až do tohoto render(), proto podmínka na šablonu.

Toho se asi nezbavím jen tak tedy.

m.brecher
Generous Backer | 911
+
0
-

@Damo

Komponenta SelectImageComponent pracuje ve třech funkčních módech:

a) synchronní vykreslení komponenty – ‚SelectImageComponent.latte‘

b) ajaxové do-kreslení dialogu – ‚openDialog.latte‘

c) fallback – vykreslení prázdné šablony ‚empty.latte‘ – nutné pro správnou funkci složitého celku

Řešení

Předložený kód není dobře strukturovaný a nemá žádné explicitní logické řízení. Není zřejmé jak a proč co funguje nebo nefunguje. Doporučuji kód doplnit o explicitní logické řízení, a úplně odebrat z factory odpovědnost za řízení, měla by pouze vytvářet komponentu, řízení by mělo být odvozeno z eventů. Dále doporučuji striktně dodržet separaci odpovědností metod render() a handleXx(), příprava dat pro vykreslování – render(), logické řízení – handle()

Vykreslování budeme řídit pomocí $renderState, který bude mít 3 hodnoty:

'render_component' - základní, synchronní vykreslení
'render_dialog' - ajaxové dokreslení dialogu
'render_empty' - fallback, prázdná data, z důvodu komplikovaného flow nutné

Přepracovaný kód komponenty:

class SelectImageComponent extends BaseComponent
{
    private string $renderState;

    public function __construct(private ?BaseComponentPayload $baseComponentPayload = null)   // moderní PHP 8.0 syntax
	{
        $this->renderState = $baseComponentPayload ? 'render_component' : 'render_empty';
    }

    public function handleOpenDialog(): void
	{
		$this->renderState = 'render_dialog';
    }

    public function render(): void
	{
		if ($this->renderState === 'render_component') {
            $this->template->setFile(dirname(__FILE__) . "/SelectImageComponent.latte");

            $this->template->unique = uniqid();
            $this->template->componentName = $this->baseComponentPayload->getComponentName();
            $this->template->className = $this->baseComponentPayload->getClassName();
		}

		if ($this->renderState === 'render_dialog') {
            $this->template->setFile(dirname(__FILE__) . "/openDialog/openDialog.latte");

            $filesBrowserPayload = new FilesBrowserPayload('selectImage');
            $filesBrowserPayload->setAsDialog();
            $this->template->filesBrowser = $filesBrowserPayload;
            $this->redrawControl('mcCanvas');
		}

        if ($this->renderState === 'render_empty') {
            $this->template->setFile(__DIR__ . '/empty.latte');
        }

        $this->template->render();
    }
}

Poznámka: vyzkoušej a pošli feedback jaktodopadlo, pošli také kód factory, nějak se mě to nezdá, tam by mohl také být problém.

Editoval m.brecher (20. 10. 22:22)

Damo
Člen | 69
+
0
-

Funguje to, až na drobnost, redraw $this->redrawControl('mcCanvas'); musi byt v handle, jinak se render() nevyvolá.

<?php

declare(strict_types=1);

namespace App\Components\Phoenix\SelectImage;

use App\AdminModule\Models\PhoenixModels\FilesBrowser\Entities\FilesBrowserPayload;
use App\Components\BaseComponent;
use App\Models\BaseComponentPayload;

class SelectImageComponent extends BaseComponent {
    private string $renderState;
    public function __construct(private readonly ?BaseComponentPayload $baseComponentPayload = null) {
        $this->renderState = $baseComponentPayload ? 'render_component' : 'render_empty';
    }

    public function handleOpenDialog(): void {
        $this->renderState = 'render_dialog';
        $this->redrawControl('mcCanvas');
    }

    public function render(): void {
        if ($this->renderState === 'render_component') {
            $this->template->setFile(dirname(__FILE__) . "/SelectImageComponent.latte");

            $this->template->unique = uniqid();
            $this->template->componentName = $this->baseComponentPayload->getComponentName();
            $this->template->className = $this->baseComponentPayload->getClassName();
        }

        if ($this->renderState === 'render_dialog') {
            $this->template->setFile(dirname(__FILE__) . "/openDialog/openDialog.latte");

            $filesBrowserPayload = new FilesBrowserPayload('selectImage');
            $filesBrowserPayload->setAsDialog();
            $this->template->filesBrowser = $filesBrowserPayload;
        }

        if ($this->renderState === 'render_empty') {
            $this->template->setFile(__DIR__ . '/empty.latte');
        }

        $this->template->render();
    }
}

Factory je multiplier, abych mohl pouzit vicekrat v sablone, zatim vypada takto

protected function createComponentPhoenixSelectImage(): Multiplier {
        return new Multiplier(function(string $id): SelectImageComponent {
            $data = $this->selectImages[$id] ?? null;

            if (!$data) {
                return new SelectImageComponent(null);
            }

            $payload = new BaseComponentPayload($data['componentName']);
            $payload->addClassName([
                'mb-1',
                'ratio',
                'ratio-16x9',
                ...($data['className'] ?? []),
            ]);

            return new SelectImageComponent($payload);
        });
    }

Editoval Damo (21. 10. 8:13)

m.brecher
Generous Backer | 911
+
0
-

@Damo

Factory metoda ukazuje, že logické rozhodování, zda se komponenta vykreslí nebo nevykreslí (vykreslí empty) provádí řádek kódu:

$data = $this->selectImages[$id] ?? null;

to je array property presenteru, ale nic o ní nevíme, pošli prosím ještě kód presenteru, ze kterého vyplyne jak se data $selectImages vytvářejí a jaký mají účel v komponentě.

Předpokládám, že foreach v latte šabloně volá factory metodu presenteru, ta vytváří komponenty, které se mají vykreslit, nebo komponenty, které se vykreslit nemají. Pokud Latte narazí na tag vykreslení komponenty tak prostě chce instanci komponenty i když se ta komponenta vlastně vykreslovat nemusí/nemá. Nejde místo komponenty poslat null, a latte si snad nějak poradí – neporadí. Komponentu dodat musíš, latte na ní zavolá render, teoreticky by render nemusel dělat vůbec nic – to ti ale nefunguje a tak necháme vykreslit prázdnou šablonu.

Pojďme pátrat směrem k $selectImages proč některou komponentu chce vykreslit a jinou ne ! Pak můžeme kód předělat tak, aby bylo jasně vidět, kde vzniká logické řízení které komponenty se ve foreach vykreslují a které ne a jestli by šlo úplně vynechat vykreslování prázdné šablony.

Damo
Člen | 69
+
0
-

Žel, je to soustava zanořených komponent, Presenter, je ještě 2 komponenty před. Klidně kdybyste byl ochoten, můžeme se spojit, můžu to celé nasdílet a ukázat.

m.brecher
Generous Backer | 911
+
0
-

@Damo

asi nemá cenu takto složitý komplex ajax-snippet-komponent zgruntu přepisovat, pokud to funguje jak má. To že se vykresluje empty komponenta vyplývá z té vnitřní složitosti, a pokud je to v kódu jasně vidět, tak bych se tím netrápil. Můžeš přepsat factory metodu tak, aby bylo vidět řízení typu vykreslované komponenty:

Pojmenuj třídu komponenty vhodnějším názvem, třeba ImageSelector (místo SelectImageComponent), logické řízení vyjádříš ve factory metodě proměnnou $renderType, která bude mít 3 hodnoty: ‚selector_body‘, ‚selector_dialog‘ a ‚selector_empty‘.

Třídu ImageSelector uprav, a) přejmenuj $renderState na vhodnější $renderType, b) doplň metodu setRenderType(), c) odeber inicializaci $renderType v konstruktoru – tu nahradí setter, d) aktualizuj hodnoty $renderType v ifech

Zde je upravená factory metoda, aby bylo vidět řízení vykreslovaného výstupu komponenty:


protected function createComponentImageSelector(): Multiplier
{
	$creator = function(string $id): ImageSelector {
		$data = $this->selectImages[$id] ?? null;

		if ($data) {
			$renderType = 'selector_body'
			$payload = new BaseComponentPayload($data['componentName']);
			$classes = $data['className'] ?? []
			$payload->addClassName('mb-1', 'ratio', 'ratio-16x9', ...$classes);

		}else{
			$renderType = 'selector_empty'
			$payload = null;
		}

		$selector = new ImageSelector($payload);
		$selector->setRenderType($renderType)

		return $selector;
	}

	return new Multiplier($creator);
}

Editoval m.brecher (21. 10. 21:10)

Damo
Člen | 69
+
0
-

Perfektní, díky za pomoc