Oddělení šablony formuláře

Fires
Člen | 89
+
0
-

Zdravím, prosím o pomoc. Rád bych si v projektu udržoval nějaký řád a pořádek a napadlo mě že by bylo dobré si formuláře oddělit do separátních latte template. Ale nedaří se mi donutit nette aby měl formulář zvláštní latte šablonu. Formulář vykresluji manuálně. Jedná se o login form kde jsou pod heslem ještě odkazy (vytvoření účtu, zapomenuté heslo) takže potřebuji trochu volnosti při renderování a nemohu? použít {control loginForm}. Je na toto nějaký „recept“? Zkusil jsem dokumentaci, dokonce chatgpt a nic :). Napadlo mě pro celý login form vytvořit zvláštní komponentu ale nevím jak do ní poté zabalit/zavolat onSuccess formuláře. Jaký je best practice u tohoto problému ?

Předem díky.

m.brecher
Generous Backer | 764
+
0
-

@Fires

Ale nedaří se mi donutit nette aby měl formulář zvláštní latte šablonu. Formulář vykresluji manuálně.

Když se formulář vykresluje manuálně tak se právě vykresluje pomocí Latte, druhou možností je vestavěný Renderer, ale tam jsou možnosti omezené – jak píšeš. Manuální vykreslování formulářů pomocí Latte je celkem kvalitně popsáno v dokumentaci -tag n:name a další:

https://doc.nette.org/…ms/rendering#…

Tag {control ‚formName‘} vykresluje obecně komponentu poděděnou z Nette\Application\UI\Control s použitím její šablony, ale když je komponentou formulář, použije se vestavěný Renderer, kde je minimální volnost cokoliv upravovat. Dá se ale tento jednoduše vykreslený formulář vsadit do šablony, kde se doplní odkazy, které potřebuješ.

Tag {control ‚formName‘} může vykreslovat do šablony i formulář, musíš ale potom formulář umístit do nadřazené komponenty poděděné z třídy Nette\Application\UI\Control, ale stejně budeš muset šablonu ručně nakódovat. Je to o něco složitější.

Pro login formulář je nejjednodušší jít cesto manuálního vykreslení pomocí latte, jak jsem uvedl v odkazu výše.

Šablonu formuláře si můžeš umístit do separátního souboru a vkládat pomocí {include …}

Můžu Ti poslat i podrobnější ukázky kódu jak vytvořit formulář zabalený do Nette\Application\UI\Control, tak kdyžtak napiš, ale zkus tohle jednoduché co Jsem Ti poslal.

Editoval m.brecher (11. 4. 2023 19:11)

Fires
Člen | 89
+
0
-

Jo, k těmto možnostem jsem také došel po pročtení dokumentace. Jednoduché řešení {include} mě hloupě nenapadlo. Rendery jsou super ale bohužel zatím jsem v mém projektu nenarazil na místo kde bych ho použil a stále si formuláře koduji ručně a renderuji n:name etc. Vždy narazím ve formuláři na nějakou design věc(css class nebo tak) a nechci class přidávat do továrny fomuláře. Příjde mi to jako míchání logiky s designem. Include bude asi má cesta, v celkovém designu bude méně kódu. Děkuji moc opět za vyčerpávající odpověď.

m.brecher napsal(a):

@Fires

Ale nedaří se mi donutit nette aby měl formulář zvláštní latte šablonu. Formulář vykresluji manuálně.

Když se formulář vykresluje manuálně tak se právě vykresluje pomocí Latte, druhou možností je vestavěný Renderer, ale tam jsou možnosti omezené – jak píšeš. Manuální vykreslování formulářů pomocí Latte je celkem kvalitně popsáno v dokumentaci -tag n:name a další:

https://doc.nette.org/…ms/rendering#…

Tag {control ‚formName‘} vykresluje obecně komponentu poděděnou z Nette\Application\UI\Control s použitím její šablony, ale když je komponentou formulář, použije se vestavěný Renderer, kde je minimální volnost cokoliv upravovat. Dá se ale tento jednoduše vykreslený formulář vsadit do šablony, kde se doplní odkazy, které potřebuješ.

Tag {control ‚formName‘} může vykreslovat do šablony i formulář, musíš ale potom formulář umístit do nadřazené komponenty poděděné z třídy Nette\Application\UI\Control, ale stejně budeš muset šablonu ručně nakódovat. Je to o něco složitější.

Pro login formulář je nejjednodušší jít cesto manuálního vykreslení pomocí latte, jak jsem uvedl v odkazu výše.

Šablonu formuláře si můžeš umístit do separátního souboru a vkládat pomocí {include …}

Můžu Ti poslat i podrobnější ukázky kódu jak vytvořit formulář zabalený do Nette\Application\UI\Control, tak kdyžtak napiš, ale zkus tohle jednoduché co Jsem Ti poslal.

Kamil Valenta
Člen | 762
+
0
-

A v čem že je ta „složitost“ mít form v komponentě a zavolat to kde si zamanu? Includovat šablonu v latte mi připadá jako krok zpět (prakticky si „znemožníš“ předhodit tomu jinou šablonu, podědit ji, refactorovat umístění šablony atp.)

Editoval Kamil Valenta (11. 4. 2023 21:58)

m.brecher
Generous Backer | 764
+
0
-

@KamilValenta

A v čem že je ta „složitost“ mít form v komponentě a zavolat to kde si zamanu?

Jó máš pravdu, ale když začíná, může si vyzkoušet tuto cestu a o měsíc později From v komponentě, což je opravdu asi nejlepší. Pošlu mu ukázku kódů.

m.brecher
Generous Backer | 764
+
-1
-

@Fires

posílám takové zjednodušené kopyto, jak udělat moderní best practice formulář, který je v samostatné třídě zapouzdřený do rodičovské komponenty. Kód aplikace se zpřehlední – formuláře jsou v samostatných souborech a mají vlastní šablony v samostatných souborech. Jsou tam další vychytávky, ale je ale potřeba si to doladit, doplnit a upravit.

Třída formuláře

  • formulář je zapouzdřen v třídě ArticleForm, což je vykreslovací komponenta poděděná z Nette\Application\UI\Control
  • konstruktorem získá a) služby (modelovou třídu), b) parametry presenteru předané prostřednictvím Factory
  • formulář umí a) vytvořit nový řádek tabulky (akce presenteru create), b) editovat existující řádek tabulky (akce presenteru update)
  • formulář umí mazat řádek tabulky – tlačítko ‚deleteButton‘
  • tři handlery pro nový záznam, editaci existujícího záznamu a mazání záznamu jsou navěšeny přímo na tlačítka – nejpřehlednější řešení
  • 90% formulářů potřebuje modelovou třídu – položky select inputů, defaultní data, modelová třída se předá jako služba
namespace App\Forms;

use App\Model\ArticleModel;
use Nette\Application\UI\Control;
use Nette\Application\UI\Form;
use Nette\Database\Table\ActiveRow;
use Nette\Utils\ArrayHash;

class ArticleForm extends Control
{
    public function __construct(
        private  ArticleModel $articleModel,  // injected as service
        private ActiveRow $article,           // passed from factory
        private bool $update,                 // passed from factory
    )
    {}

    /* build form structure */

    public function createComponentForm(): Form
    {
        $form = new Form();

        //.... adding form inputs ....

        $form->addSelect('items', 'Položky', $this->ArticleModel->getItems());  // model class used

        if($this->update){
            $form->addSubmit('updateButton', 'Uložit')
                ->onClick[] = $this->updateArticle(...);  // syntax PHP >= 8.1

            $form->addSubmit('deleteButton', 'Smazat')
                ->setValidationScope([])
                ->onClick[] = $this->deleteArticle(...);

            $form->onRender[] = fn() => $form->setDefaults($this->article); //  model class used

        }else{
            $form->addSubmit('createButton', 'Uložit')
                ->onClick[] = $this->createArticle(...);
        }

        return $form;
    }

    /* submit form handlers - by buttons */

    public function createArticle(ArrayHash $values)
    {
        $article = $this->articleModel->createOne($values);
        //  next processing
    }

    public function updateArticle(ArrayHash $values)
    {
        $updated = $this->articleModel->updateOne($values);
        //  next processing
    }

    public function deleteArticle()
    {
        $deleted = $this->articleModel->deleteOne($this->article);
        //  next processing
    }

    /* rendering */

    public function render(string|null $formHeading)  // variable from {control} latte tag
    {
        $this->template->formHeading = $formHeading;
        $this->template->render('form-template.latte');  // setting form template file
    }
}

Factory Interface

  • zaregistruje se jako služba
  • použije se v presenteru pro vytvoření formuláře ArticleForm
  • zajistí předání parametrů z presenteru do konstruktoru formuláře
namespace App\Factories;

use App\Forms\ArticleForm;
use App\Model\ArticleModel;
use Nette\Database\Table\ActiveRow;

interface ArticleFormFactory
{
    public function create(
        ActiveRow|null $article,
        bool $update
    ): ArticleForm;
}

Modelová třída

  • zjednodušený příklad
namespace App\Model;

use Nette\Database\Explorer;
use Nette\Database\Table\ActiveRow;
use Nette\Utils\ArrayHash;

class ArticleModel
{
    public function __construct(private Explorer $database)
    {}

    public function getItems(): array
    {
        return  $this->database->table('items')->fetchPairs('id', 'caption');
    }

    public function getOne(int $id): ActiveRow|null
    {
        return  $this->database->table('article')->get($id);
    }

    public function createOne(ArrayHash $values): ActiveRow
    {
        return $this->database->table('article')->insert($values);
    }

    public function updateOne(ActiveRow $article, ArrayHash $values): bool
    {
        return $article->update($values);
    }


    public function deleteOne(ActiveRow $article): int
    {
        return $article->delete();
    }

}

Presenter

  • nechá si předat factory formuláře jako službu
  • má editační metody create/update a property $update kterou využije formulář (hodí se i v šabloně akce)
  • editovaný řádek tabulky musí ověřit v actionUpdate metodě existenci řádku
  • jednou získaný řádek tabulky předá presenter kam je potřeba a) do šablony b) do factory formuláře
  • formulář vytvoří pomocí factory které předá parametry pro formulář, ona je předá dál
namespace App\Presenters;

use App\Factories\ArticleFormFactory;
use App\Forms\ArticleForm;
use App\Model\ArticleModel;
use Nette\Database\Table\ActiveRow;

class TestArticlePresenter extends BasePresenter
{
    private bool $update = false;

    private ActiveRow|null $article = null;

    public function __construct(
        private ArticleModel $articleModel,
        private ArticleFormFactory $articleFormFactory,
    )
    {}

    public function actionUpdate(int $id)
    {
        $this->update = true;
        $this->article = $this->articleModel->getOne($id);
        if(!$this->article){
            $this->error('Page not found', 404);
        }
        $this->template->article = $this->article;
    }

    public function createComponentArticleForm(): ArticleForm
    {
        return $this->articleFormFactory->create(
            article:  $this->article,  //  use already available data, prevent duplicite sql query
            update: $this->update,     // needed by multifunctional form for creating + updating data
        );
    }
}

Šablony akcí

tag {control} vykreslí formulář ze šablony, ta je v samostatném souboru (nastavuje se v ArticleForm::render())
podle potřeby lze předat specifické texty podle akce create/update – např. nadpis formuláře

a) akce create

{block 'content'}
    {control 'articleForm', formHeading: 'Nový článek'}
{/block}

b) akce update

{block 'content'}
    {control 'articleForm', formHeading: 'Editace článku'}
{/block}

Šablona formuláře

form-template.latte

<div class="form-container">
   <h3 class="form-heading">{$formHeading}</h3>
   <div class="form-subcontainer">
      <form n:name="articleForm">
         ......
      </form>
   </div>
</div>

Editoval m.brecher (12. 4. 2023 3:06)

h4kuna
Backer | 740
+
+4
-

@m.brecher Prosím tě, nenazývaj to

jak udělat moderní best practice formulář

Ukázka má několik trhlin na kráse a ostatní začátečníky by to mohlo mást.

  1. instancování samotného formuláře by jsi měl mít ve vlastní factory
$form = new Form();

Budeš mít hromadu formulářů, třeba 200+

  • jak ke všem přidáš překladač
  • jak vyměníš třídu, když si uděláš potomka
  • jak přidáš vlastní renderer
  1. Proč je zde parametr $update
class ArticleForm extends Control
{
    public function __construct(
        private ArticleModel $articleModel,  // injected as service
        private ActiveRow $article,           // passed from factory
        private bool $update,                 // passed from factory
    )
    {}

Nestačilo by $article udělat nullable? Co jsem koukal tak stačilo. Tím se zjednoduší TestArticlePresenter a ArticleFormFactory.

Editoval h4kuna (12. 4. 2023 8:05)

h4kuna
Backer | 740
+
+1
-

@Fires Nesnaž se dogmaticky oddělovat formuláře do vlastních šablon, 90% formulářů použiješ jen jednou, tak není důvod je nedat rovnou do šablony presenteru. Pokud se změní situace, tak vytáhnout ten jeden formulář a přesunout ho do komponenty je jednoduché. Ale dělat to od začátku pro všechny formy je jen zbytečně pracné a stává se nepřehledné.

Pokud se bavíme o loginFormu a toho co jsi popisoval, tak si zase myslím, že není potřeba mít formulář v komponentě, ale opět ho vykreslit v šabloně presenteru, protože budou vypadat jinak jak jsi popsal. Pokud chceš mít ty dvě různé šablony vedle sebe, tak pak ano je potřeba z toho udělat komponentu, ale s možností měnit soubor se šablonou.

Stále platí, že bych dogmaticky neextrahoval formuláře do vlastní tříd děděných od UI\Control (komponenta).

Příklad:

Já si dělám tovární třídy pro formuláře bez závislosti na UI\Control

use Nette\Application;

final class FormFactory
{

	public function create(): Application\UI\Form
	{
		return new Application\UI\Form();
	}

}

Tvorba konkretního formuláře, pak máš volnost zda ArticleFormFactory si dáš závislost do presenteru nebo závislost do vytvořené vlastní třídy děděné od UI\Control.

final class ArticleFormFactory
{
	public function __construct(
		private ArticleModel $articleModel,
		private FormFactory $factory,
	)
	{
	}


	public function create(?ActiveRow $article = null): Nette\Application\UI\Form
	{
		$form = $this->factory->create();
		$form->addSelect('items', 'Položky', $this->articleModel->getItems());
		if ($this->article !== null) {
			$form->addSubmit('updateButton', 'Uložit')
				->onClick[] = $this->updateArticle(...);
			$form->addSubmit('deleteButton', 'Smazat')
				->setValidationScope([])
				->onClick[] = fn($button, $values) => $this->deleteArticle($values, $article);
			$form->setDefaults($article);
		} else {
			$form->addSubmit('createButton', 'Uložit')
				->onClick[] = $this->createArticle(...);
		}

		return $form;
	}


	/* submit form handlers - by buttons */
	private function createArticle(ArrayHash $values)
	{
		$article = $this->articleModel->createOne($values);
		//  next processing
	}


	private function updateArticle(ArrayHash $values)
	{
		$updated = $this->articleModel->updateOne($values);
		//  next processing
	}


	private function deleteArticle(ArrayHash $values, ?ActiveRow $article)
	{
		$deleted = $this->articleModel->deleteOne($article);
		//  next processing
	}

}

Editoval h4kuna (12. 4. 2023 12:07)

m.brecher
Generous Backer | 764
+
0
-

@h4kuna

Pokud se bavíme o loginFormu a toho co jsi popisoval, tak si zase myslím, že není potřeba mít formulář v komponentě, ale opět ho vykreslit v šabloně presenteru, protože budou vypadat jinak jak jsi popsal. Pokud chceš mít ty dvě různé šablony vedle sebe, tak pak ano je potřeba z toho udělat komponentu, ale s možností měnit soubor se šablonou.

Tohle přesně jsem doporučoval hned na začátku – pro obyčejný loginForm to úplně stačí, @Fires chtěl naopak loginForm z presenteru oddělit tak, aby šablona byla v samostatném formuláři – poradil jsem mu použít {include}.

Pak do diskuse vstoupil @KamilValenta a doporučil formulář na bázi UI\Control jako nejlepší řešení a tak jsem poslal takovou zjednodušenou ukázku. Tam jsem už na mysli neměl jednoduchý loginForm, ale obecně jakýkoliv formulář.

Pokud chceš mít ty dvě různé šablony vedle sebe, tak pak ano je potřeba z toho udělat komponentu

dvě šablony které jsem uváděl a) a b) jsou šablony akcí presenteru actionCreate() a actionUpdate(), formulář má jednu šablonu pro obě akce, ale modifikovanou pomocí proměnné $formHeading

Prosím tě, nenazývaj to jak udělat moderní best practice formulář … Ukázka má několik trhlin na kráse a ostatní začátečníky by to mohlo mást.

moderní best practice opravuji na moderní, nováčky by to mohlo zmást, možností je více.

Proč je zde parametr $update ??

Dříve jsem nerozlišoval akce create a update v presenteru a používal jednu akci edit, podle nullability $id presenter poznal, o kterou akci se jedná. Toto schéma jsem opustil jako málo přehledné a náchylné k chybám a akce prováděné s formulářem nyní rozlišuji v akcích presenteru a $id už je pouze informací o editovaném řádku. $update jde vynechat, ale kód bude méně přehledný

instancování samotného formuláře by jsi měl mít ve vlastní factory

nevím, jestli to správně chápu, ale tak, jak jsem to poslal má každá formulář svoji vlastní UI\Control a factory vytvoří za běhu Nette díky factory interface (oficiálně popsaný postup v dokumentaci Nette), formulář ještě oddělit do další factory jak píšeš – není mě jasné jaký by byl přínos ?? Neměl Jsi spíš na mysli jiný návrhový vzor formulářů, kdy se formulář nevytváří s použitím UI\Control, ale pomocí factory ?? – to je také jedna z možností.

formulář vytvořený uvnitř presenteru

mě se to také líbí a ze začátku jsem to používal, ale časem mě začalo vadit, že formuláře nejsou v projektu vidět jako samostatné soubory, jdou ale oddělit do traity, což je také jedna z možností.

Editoval m.brecher (12. 4. 2023 12:49)

Kamil Valenta
Člen | 762
+
0
-

m.brecher napsal(a):

Pak do diskuse vstoupil @KamilValenta a doporučil formulář na bázi UI\Control jako nejlepší řešení a tak jsem poslal takovou zjednodušenou ukázku. Tam jsem už na mysli neměl jednoduchý loginForm, ale obecně jakýkoliv formulář.

To prosím nespojovat. Já jsem nepsal o žádném nejlepším řešení pro jakýkoliv formulář. Napsal jsem, že u formulářů, které se vykreslují opakovaně na více místech, je lepší komponenta než includovaná šablona, důvody a že to není o nic složitější. Z mého pohledu zrovna loginForm může být nejžhavější adept na obalení komponentou, pokud má být přihlášení dostupné v nějakém plovoucím baru.
A articleForm bych zrovna do komponenty nedal, protože ten se těžko použije jinde, než v jedné actioně.

h4kuna
Backer | 740
+
0
-

@mbrecher obávám se že mícháš jablka s hruškama

Tohle přesně jsem doporučoval hned na začátku – pro obyčejný loginForm to úplně stačí, @Fires chtěl naopak loginForm z presenteru oddělit tak, aby šablona byla v samostatném formuláři – poradil jsem mu použít {include}.

{include} jsem nikdy nepoužil pro oddělení html / rendrování, vyjadřovat se nebudu

Pak do diskuse vstoupil @KamilValenta a doporučil formulář na bázi UI\Control jako nejlepší řešení a tak jsem poslal takovou zjednodušenou ukázku. Tam jsem už na mysli neměl jednoduchý loginForm, ale obecně jakýkoliv formulář.

Řešení to může být dobré, ale ne tak jak jsi ho navrhnul.

dvě šablony které jsem uváděl a) a b) jsou šablony akcí presenteru actionCreate() a actionUpdate(), formulář má jednu šablonu pro obě akce, ale modifikovanou pomocí proměnné $formHeading

$formHeading je nějaká externí závislost kterou, lze vyřešit podmínkou jestli je $article === null, pokud bude article nullable, v šabloně nechceš volat komponentu a vyplňovat ji zbytečné parametry, většinou to jde jinak

Dříve jsem nerozlišoval akce create a update v presenteru a používal jednu akci edit, podle nullability $id presenter poznal, o kterou akci se jedná. Toto schéma jsem opustil jako málo přehledné a náchylné k chybám a akce prováděné s formulářem nyní rozlišuji v akcích presenteru a $id už je pouze informací o editovaném řádku. $update jde vynechat, ale kód bude méně přehledný

já je taky nerozlišuju, mně není jasný jak naplníš proměnnou private ActiveRow $article když $update je false, buď mám id a mohu si nechat vytvořit entitu nebo to prostě nemám, žádný další složitosti.

Prosím tě, nenazývaj to jak udělat moderní best practice formulář … Ukázka má několik trhlin na kráse a ostatní začátečníky by to mohlo mást.

moderní best practice opravuji na moderní, nováčky by to mohlo zmást, možností je více.

Spíše bych to nazval Mnou používané řešení protože by to nemělo vyznívat, že co jsi poslal používá komunita. Protože v tom máš svoje implementační závislosti, který jsi ochotný používat.

instancování samotného formuláře by jsi měl mít ve vlastní factory

nevím, jestli to správně chápu, ale tak, jak jsem to poslal má každá formulář svoji vlastní UI\Control a factory vytvoří za běhu Nette díky factory interface (oficiálně popsaný postup v dokumentaci Nette),

Ale tady stále neodpovídáš na otázku jak vyměníš třídu formu pokud si uděláš potomka Form, nebo formu budeš chtít přidat závislost například na translator, v čase i na něco jiného a těch formů budeš mít mega hodně viz mnou popsaná FormFactory

formulář ještě oddělit do další factory jak píšeš – není mě jasné jaký by byl přínos ??

Pokud máš na mysli ArticleFormFactory tak ten je v tom přenosu mezi Presenterem a UI\Control, jasně definovaná závislost ke zpracování formu a nejsou zde závislosti související s vykreslením

Neměl Jsi spíš na mysli jiný návrhový vzor formulářů, kdy se formulář nevytváří s použitím UI\Control, ale pomocí factory ?? – to je také jedna z možností.

Nevím co je návrhový vzor formulářů a celé té větě nezozumím. Já se snažím UI\Control do toho vůbec nemíchat. Není to potřeba ve spoustě případů.

formulář vytvořený uvnitř presenteru

mě se to také líbí a ze začátku jsem to používal, ale časem mě začalo vadit, že formuláře nejsou v projektu vidět jako samostatné soubory, jdou ale oddělit do traity, což je také jedna z možností.

Tohle jsem ani jednou nenapsal a formuláře v traitě nedoporučuju, kvůli DI. Doporučuju mít form ve vlastní factory viz ArticleFormFactory.

Editoval h4kuna (12. 4. 2023 14:37)

mskocik
Člen | 53
+
-1
-

Ja som tento use case riešil takto:

class TemplatedAppForm extends UI\Form
{
    protected $template = null;
    protected $templateFile = null;
    protected $templateProps = [];

    public function __construct($container = null)
    {
        parent::__construct($container);
        $this->monitor(Presenter::class, function(Presenter $presenter) {
            $this->template = $presenter->getTemplateFactory()->createTemplate();
        });
    }

    public function render(...$args): void
    {
        $this->fireRenderEvents();
        $this->template->setFile($this->templateFile);
        count($this->templateProps) > 0 && $this->template->setParameters($this->templateProps);
        $this->template->renderer = $this->getRenderer();
        $this->template->form = $this;
        echo $this->template->render();
    }
}