Formulář jako znovupoužitelná komponenta

Upozornění: Tohle vlákno je hodně staré a informace nemusí být platné pro současné Nette.
Šaman
Člen | 2634
+
0
-

Protože jsem na tom nějaký ten den strávil a protože v tomto spousta začátečníků lítá, tak sem hodím návod jak vytvořit znovupoužitelnou komponentu formuláře. Oproti prvotní minimalistické verzi se mi komponenta trochu rozrostla, snahou bylo DRY při opakovaném použití. (Většinu takto stavěných formulářů používám dvakrát. Ve Front modulu a pak znovu v administraci.)

Budu popisovat tvorbu komponenty TestFormComponent. Jako základ použiju skeleton.

Soubor /components/TestForm.php:

<?php
class TestForm extends Control
{
	/* vychozi hodnoty */
	public $defaults = array();

	/* nastaveni komponenty */
	public $settings = array();

	/* promenne pro obsluhu udalosti */
	public $saveData = null;
	public $onOkClick = array();
	public $onCancelClick = array();

	/** v konstruktoru urcime defaultni nastaveni */
	public function  __construct($defaults = null)
	{
		parent::__construct();

		if (isset ($defaults))
			$this->defaults = $defaults;

		$this->onOkClick[] = callback($this, 'okClick');
		$this->onCancelClick[] = callback($this, 'cancelClick');
	}

	/** metoda pro vykresleni komponenty */
	public function render()
	{
		$template = $this->template;
		$template->setFile(dirname(__FILE__) . '/testTemplate.phtml');
		$template->render();
	}

	/** tovarnicka na formular */
	public function createComponentTestForm($name)
	{
		$form = new AppForm($this, $name);

		// pridame nejaka formularova policka
		$form->addText('input', 'popisek inputu')
			->addRule(Form::MAX_LENGTH, 'Maximální délka smí být %d znaků', 8)
			->addRule(~Form::NUMERIC, 'Čísla se neuznávají');

		// dulezite je odesilaci tlacitko
		$form->addSubmit('ok', 'OK')
			->onClick = $this->onOkClick;

		// zobrazeni CancelButtonu se da vypnout v presenteru prepinacem: settings['cancelButtonDisabled'] = TRUE;
		if (!isset($this->settings['cancelButtonDisabled']) || $this->settings['cancelButtonDisabled'] == FALSE)
		{
			$form->addSubmit('cancel', 'Cancel!')
				->setValidationScope(FALSE) // formular se nebude validovat
				->onClick = $this->onCancelClick;
		}

		// pokud jeste nedoslo k odeslani, tak pri vykresleni formulare nastavime vychozi hodnoty
		if(!$form->isSubmitted())
			$form->setDefaults($this->defaults);

		return $form;
	}

	/** Zpracovani formulare po kliknuti na tlacitko "Ok" */
	public function okClick(SubmitButton $button)
	{
		debug::fireLog('Prednastavena obsluha udalosti onOkClick v komponente.');

		/* ziskani hodnot z formulare */
		$values = $button->getForm()->getValues();
		debug::fireLog($values);

		/* nacteni hodnot $setings */
		$settings = $this->settings;
		debug::fireLog($settings);

		/* zkusime data ulozit */
		if(isset($this->saveData))
		{
			call_user_func($this->saveData, $button); // misto promenne button je mozne predavat rovnou data k ulozeni, to je na vas
		}
		elseif(end($this->onOkClick) == callback($this, 'okClick'))
		{
			throw new Exception("Nemohu ulozit data z formulare! Neni k dispozici metoda k ulozeni dat, ani jiz nenasleduje zadna obsluha v presenteru.");
		}

		/* pokud je prave vykonavana metoda jedinou obsluhou udalosti, tak implicitne presmerovat */
		if(end($this->onOkClick) == callback($this, 'okClick'))
			$this->getPresenter()->redirect("this");
	}

	/** Zpracovani formulare po kliknuti na tlacitko "Cancel" */
	public function cancelClick(SubmitButton $button)
	{
		debug::fireLog('Prednastavena obsluha udalosti onCancelClick v komponente.');

		/* pokud je prave vykonavana metoda jedinou obsluhou udalosti, tak implicitne presmerovat */
		if(end($this->onCancelClick) == callback($this, 'cancelClick'))
			$this->getPresenter()->redirect("this");
	}

}
?>

Soubor /components/TestFormTemplate.phtml:

<?php
{control $form begin}
{control $form errors}

<p>{$form['input']->label} ... {$form['input']->control}</p>
<p>{$form['ok']->control} {$form['cancel']->control}<p>

{control $form end}
?>

Soubor /components/TestTemplate.phtml:

<?php
{include 'testFormTemplate.phtml', form => $control['testForm']}
?>

A nakonec jak tuto komponentu používáme v presenteru:

<?php
class HomepagePresenter extends BasePresenter
{

	public function renderDefault()
	{
	}

	public function createComponentTestForm()
    {
        $cmp =  new TestForm();

		$cmp->defaults['input'] = "default";

		$cmp->settings['cislo'] = 1; // do promenne `settings` lze zapisovat libovolna data ktera ovlivni vykresleni/chovani komponenty (zpracovani je jen na programatorovi)

		$cmp->saveData = array($this, 'testFormSaveData');

		//$cmp->onOkClick = array(); // vypne defaultni zpracovani signalu (ktery na nej zavesila komponenta)
		$cmp->onOkClick[] = callback($this, 'testFormOkClick'); // navesi obsluhu signalu onOkClick (prida metodu na konec seznamu)

		//$cmp->onCancelClick = array(); // vypne defaultni zpracovani signalu (ktery na nej zavesila komponenta)
		$cmp->onCancelClick[] = callback($this, 'testFormCancelClick'); // navesi obsluhu signalu onCancelClick (prida metodu na konec seznamu)

		return $cmp;
    }

	/** Rutina pro ukladani dat z formulare */
	public function testFormSaveData(SubmitButton $button)
	{
		debug::fireLog('Rutina pro ukladani dat z formulare v presenteru.');

		/** ziskani hodnot z formulare */
		$values = $button->getForm()->getValues();
		debug::fireLog($values);

		/** nacteni hodnot $setings */
		$settings = $button->getForm()->getParent()->settings;
		debug::fireLog($settings);
	}

	/** Zpracovani formulare po kliknuti na tlacitko "Ok" v presenteru */
	public function testFormOkClick(SubmitButton $button)
	{
		debug::fireLog('Obsluha udalosti onOkClick v presenteru.');

		/** ziskani hodnot z formulare */
		$values = $button->getForm()->getValues();
		debug::fireLog($values);

		/** nacteni hodnot $setings */
		$settings = $button->getForm()->getParent()->settings;
		debug::fireLog($settings);

		//$this->redirect("this");
	}

	/** Zpracovani formulare po kliknuti na tlacitko "Cancel" v presenteru */
	public function testFormCancelClick(SubmitButton $button)
	{
		debug::fireLog('Obsluha udalosti onCancelClick v presenteru.');

		//$this->redirect("this");
	}

}
?>

Teď je již možné v šabloně volat komponentu jako widget.
Soubor default.phtml

<?php
{widget testForm}
?>

Obslužné metody v presenteru je možné zcela vynechat, případně minimalizovat na redirect na další krok formuláře. Místo metody SaveData() jsem předával komponentě přímo model a komponenta si data uložila sama, ale to není úplně košer. Komponenta se tím spojila s modelem (ale zase si z něho mohla cucat přednastavená data apod.) Chování formuláře lze nastavovat přímo v presenteru, ale pokud nám stačí implicitní chování (typicky např. tlačítko Cancel), tak nemusíme nic nastavovat.

Doufám, že z komentářů je patrné, jak se kód chová, pokud ne, tak dotazy a zlepšováky pište prosím sem.

Večer nebo zítra chci ještě nastudovat možnosti jak uploadovat soubor a hodit sem kompletní ukázkovou aplikaci.

Honza Kuchař
Člen | 1662
+
0
-

Ty defaults a tak je dobrý nápad, ale když si vytvoříš instalci Klasického AppForm a zavoláš nad ním ->setDefaults() tak docílíš snad tohoto samého, ne? Nebo mi něco uteklo?

Šaman
Člen | 2634
+
0
-

‚SetDefaults()‘ nastavi implicitní hodnoty z definice formuláře, ale já potřebuji často předat jiné pole defaultních hodnot (např. v administraci je pro mě defaultní to, co mám uložené v databázi.)

‚SetDefaultValue()‘ nepoužívám, ale není problém zkombinovat obě metody dohromady. Pokud jsou nastavené ‚$this->defaults‘, tak je použít, jinak volat ‚setDefaults()‘ bez parametru a tedy použít hodnoty nastevené pomocí ‚setDefaultValue()‘.


Jinak příklad má spíš ukázat, jak pracovat s těmi poli callbacků. Jen jsem zjistil, že jsem moc líný psát to jako skutečný tutoriál a přišla mi škoda ořezat příklad o ty ostatní nápady. To, co měl demonstrovat je jak mohu načíst hodnoty v komponentě a v presenteru a jak navěsit obsluhu událostí.

Honza Kuchař
Člen | 1662
+
0
-

Pořád nějak nevidím, v čem je to lepší než AppForm. Zkus to prosím shrnout do dvou jednoduchých srozumitelných vět.

Šaman
Člen | 2634
+
0
-

Aha, tak to je nedorozumění. Tohle nenahrazuje AppForm!

Je to ukázka komponenty která obaluje AppForm a která (ta komponenta) reprezentuje jeden konkrétní formulář, který se pak dá jednoduše vlepit do aplikace na několika místech (a s trochu odlišným chováním). Je to dotažený příklad z tohoto vlákna a zároveň odpověď na tu poslední otázku. Jenom se mi ten příklad trochu rozrostl, protože mám v aplikaci takových formulářových komponent víc a chtěl jsem k jejich nastavení z presenteru jednotný přístup.

A jestli se ptáš proč nedědím přímo AppForm, tak proto že

  • a. nepodařilo se mi to rozchodit bez mezivrstvy (ve které si AppForm v továrničce vyrobím).
  • b. jednodušší zápis v šabloně (nemusim includovat, ale jen vložit widget)
  • c. v té obalové můžeme např. sjednotit několik formulářů do jedné komponety, nebo přidat místo pro flashMessage, aniž by se toto muselo bastlit přímo do šablony formuláře.

Ale stručně: Tohle je funkční ukázka formuláře obaleného v komponentě. Je tam ukázáno, jak se lze k vlastnostem formuláře přistupovat z metod v komponentě i z presenteru. A je tam předvedeno, jak navěšovat callbacky jako obsluhu tlačítek. Kromě toho obsahuje několik nápadů, jak si zjednodušit zvovupoužitelnost třeba v Admin modulu (kde se ten samý formulář má chovat lehce odlišně – třeba defaults jsou pro něj data z databáze, po odeslání nemá přesměrovávat na další krok apod).

Jestli znáš jednodušší metodu, tak sem s ní, mě se tohohle výsledeku jinak dosáhnout nepodařilo.