Formuláře: BaseForm a jeho limity

Upozornění: Tohle vlákno je hodně staré a informace nemusí být platné pro současné Nette.
Vojtěch Dobeš
Gold Partner | 1316
+
0
-

Rád bych rozvinul postřeh, který může být pro mnohé samozřejmostí. Ve snaze o psaní co nejméně kódu jsem přes několik projektů dospěl k uspokojivému (nechápu, jak jsem si to mohl myslet) BaseFormu – tedy třídě extendující UI\Form a doplňující/vytvářející užitečnou funkcionalitu: šablony, znovupoužití formuláře atd. Většina formulářů v aplikaci pak od takového BaseForm extendovala. Stejně jako mají presentery BasePresenter.

A teď jsem narazil na potřebu rychle formulář zajaxovat. No sakrapes, vždyť on UI\Form nemá ani invalidateControl… a tak jsem koncept přemyslel a uvědomil si, jak o mnoho snazší a zároveň téměř totožné by bylo zavést si spíše BaseFormControl. Co bude obsahovat? Aktuálně mi vychází pár řádků:

class BaseFormControl extends UI\Control
{

	protected function template() {}

	public function render()
	{
		$args = func_get_args();
		$path = __DIR__ . '/' . ClassType::from($this)->getShortName() . '.latte';
		if (!is_file($path)) {
			return call_user_func_array(array($this['form'], 'render'), $args);
		}
		$template = $this->template;
		$template->_form = $template->form = $this['form']; // kvůli snippetům
		$template->setFile($path); // automatické nastavení šablony
		array_unshift($args, $template);
		call_user_func_array(array($this, 'template'), $args); // pro konfiguraci v potomcích
		$template->render();
	}

}

A je to. Řadový formulář pak může vypadat třeba takto (s možností šablony v SignInForm.latte):

class SignInForm extends BaseFormControl
{

	protected function createComponentForm()
	{
		$form = new UI\Form;
		...
	}

}

Přijde mi, že z vnějšku ani nejde poznat rozdíl, zatímco uvnitř se lépe využívá toho, co již má v sobě Nette obsažené (komponentový model…), a také se takový formulář snáze rozšiřuje (díky fíčurám UI\Control…).

LeonardoCA
Člen | 296
+
0
-

Zrovna jsem dnes nad tím dnes taky přemýšlel proč vlastně UI/Form nedědí od Control. Asi tvůj návrh v příštích dnech testnu.

llook
Člen | 407
+
0
-

Nastavení šablony podle mě patří spíš do createTemplate, které si potomci pro konfiguraci můžou rozšířit. Trochu bych ti to přeskupil:

class BaseFormControl extends UI\Control
{

	public function createTemplate($class = null)
	{
		$template = parent::createTemplate($class);
		if ($template instanceof \Nette\Templating\FileTemplate) {
			$path = __DIR__ . '/' . ClassType::from($this)->getShortName() . '.latte';
			$template->setFile($path); // automatické nastavení šablony
		}
		$template->_form = $template->form = $this['form']; // kvůli snippetům
		return $template;
	}

	public function render()
	{
		if ($this->template instanceof \Nette\Templating\FileTemplate
			&& !is_file($this->template->getFile())) {

			$args = func_get_args();
			return call_user_func_array(array($this['form'], 'render'), $args);
		} else {
			$this->template->render();
		}
	}

}

Pak se mi ještě nezdá ta konvence pro cestu k šabloně, že všechny šablony budou ve stejném adresáři, jako BaseFormControl. Já třeba šablony ke controlům nechávám v adresáři s daným controlem a to by se dalo napsat takhle:

$path = dirname(ClassType::from($this)->getFileName()) . '/' . ClassType::from($this)->getShortName() . '.latte';
Vojtěch Dobeš
Gold Partner | 1316
+
0
-

S tou cestou k šabloně máš naprostou pravdu, to byla moje noční ledabylost… Ad přeskupení: pro pohodlnost jsem si tam zavedl metodu template(), abych nemusel volat parent::... :). Ale je to asi vcelku jedno, tohle řešení je jednoznačně košer.

bojovyletoun
Člen | 667
+
0
-

Je to pěkné a napadlo mě, jestli není místo call_user_func_array(array($this, 'template'), $args) použít vhodnější eventy $this->onTemplate($args); Zdá se mi to lepší než dědičnost

uestla
Backer | 796
+
0
-

Ve svém skeletonu mám BaseControl ve smyslu BasePresenteru pro komponenty. Stačilo mi tedy přepnout výhybku v uvažování a spec. formuláře nedědit od BaseForm (který jsem tímto nadobro zahodil), ale právě od BaseControl.

V Šablonách pak nevkládám žádné {include _form.latte} ani podobné (za účelem vykreslení formuláře), ale krásně a čistě {control specialForm} – o vykreslení se postará šablona komponenty, přesně tak, jak by to mělo být.

Díky!

duskohu
Člen | 778
+
0
-

Zdravim, zaujal ma tento sposob pouzivania form, chcel by som sa spytat mozno blba ot. ,ale preco ked nema template savlonu, resp. ked nenajde jej subor tak sa deje toto?. Preco sa renderu predavaju parametre ked nema sablonu, vsak tym padom nema co renderovat.

if (!is_file($path)) {
    return call_user_func_array(array($this['form'], 'render'), $args);
}
Vojtěch Dobeš
Gold Partner | 1316
+
0
-

@duskohu Formulář lze renderovat pomocí makra {control}. Tahle řádka kódu pro výchozí implementaci zachovává podporu. Někdy můžu nechtít zvláštní šablonu.

duskohu
Člen | 778
+
0
-

vojtech.dobes napsal(a):

@duskohu Formulář lze renderovat pomocí makra {control}. Tahle řádka kódu pro výchozí implementaci zachovává podporu. Někdy můžu nechtít zvláštní šablonu.

lenze ked sablona neni tak vrati Missing template file

vvoody
Člen | 910
+
0
-

Myslel to asi tak že šablóna musí byť vždy, ale je v nej len jednoducho makro {control myForm} v tých prípadoch keď nám postačí len default renderer. Takto si zachováme do budúcnosti jednoduchý prechod na vlastné vykreslovanie formulára v samostatnej šablóne.

Editoval vvoody (10. 11. 2012 14:34)

Vojtěch Dobeš
Gold Partner | 1316
+
0
-

@duskohu Ukaž komplet třídu.

duskohu
Člen | 778
+
0
-

a pokial nemam pri komponente sablonu.latte tak mi vrati Missing template file.
asi som to nepochopil spravne ako puzivat vseobecnu sablonu alebo konkretne fefinovanu.
vedeli by ste mi uviest priklad? PLS.

<?php
// BaseFormControl.php
use Nette\Application\UI;

class BaseFormControl extends UI\Control {
    /* @var array nastaveni komponenty */

    public $settings = array();

    public function createTemplate($class = null) {
        $template = parent::createTemplate($class);

        if ($template instanceof \Nette\Templating\FileTemplate) {
            $path = dirname(Nette\Reflection\ClassType::from($this)->getFileName()) . '/' . Nette\Reflection\ClassType::from($this)->getShortName() . '.latte';

            $template->setFile($path); // automatické nastavení šablony
        }
        $template->_form = $template->form = $this['form']; // kvůli snippetům
        return $template;
    }

    public function render() {
        if ($this->template instanceof \Nette\Templating\FileTemplate
                && !is_file($this->template->getFile())) {

            $args = func_get_args();
            return call_user_func_array(array($this['form'], 'render'), $args);

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

}


// SignInFormControl .php

use Nette\Application\UI;

class SignInFormControl extends BaseFormControl {

    protected function createComponentForm() {
        $form = new UI\Form;

        $form->addText('email', 'Email', NULL, 30)
                ->addRule($form::FILLED, 'Pole "%label" musí byť vyplnené!')
                ->addRule($form::EMAIL, 'Je nutné zadať platnú emailovú adresu!');

        $form->addPassword('password', 'Heslo', NULL, 30)
                ->addRule($form::FILLED, 'Pole "%label" musí byť vyplnené!');

        $form->addCheckbox('persistent', 'Pamätať si ma na tomto počítači');

        $form->addSubmit('login', 'Prihlásiť sa');
        return $form;
    }

}

// Presenter
    /**
     * FORM - Sing In
     */

    protected function createComponentSignInFormControl() {
        $control = new \SignInFormControl();
        $control['form']->onSuccess[] = callback($this, 'signInFormSubmitted');
        return $control;
    }
Vojtěch Dobeš
Gold Partner | 1316
+
0
-

Tam ta úprava je vadná, setFile() se musí volat až po ověření existence toho souboru – jinak to právě tam zařve myslím.

duskohu
Člen | 778
+
0
-

Skusal som si urobit BaseFormControl ktory by som vedel pouzivat aj ked vytvaram Control class subor
aj ked by som nechcel vytvarat Controll subor ale chcel by som urobit formular priamo v presenri. V BaseFormControl by som si urcil zakladnu sablonu pre vsetky formulare.

BaseFormControl.php

use Nette\Application\UI;

class BaseFormControl extends UI\Control {

    /** @var string default template file */
    protected $defaultTemplateFile = "default";


    public function createTemplate($class = null) {
        $template = parent::createTemplate($class);
        $template->_form = $template->form = $this['form'];
        return $template;
    }

    public function render() {
        $args = array();
        if(func_num_args()>0){
            $args= func_num_args();
        }
        $template = $this->template;
        if ($this->template instanceof \Nette\Templating\FileTemplate) {

            $path = dirname(Nette\Reflection\ClassType::from($this)->getFileName()) . '/' . Nette\Reflection\ClassType::from($this)->getShortName() . '.latte';
            // ked mam definovanu sablonu pre control
            if (is_file($path)) {
                $template->setFile($path);
            }
            //defaultna sablona pre control
            else {
                if (is_file(__DIR__ . "/" . $this->defaultTemplateFile . ".latte")) {
                    $template->setFile(__DIR__ . "/". $this->defaultTemplateFile . ".latte");
                }
            }
        }
        array_unshift($args, $template);
        call_user_func_array(array($this->template, 'render'), $args);
    }

}

pokial si budem chciet vyrobit komponentu

use Nette\Application\UI;

class SignInFormControl extends BaseFormControl {

    protected function createComponentForm() {
        $form = new UI\Form;
        // Naplnim
        return $form;
    }
    // pokial nebudem chceit defaultnu sablonu tak si len pridam k suboru SignInFormControl.latte

}

a v presentru:

protected function createComponentSignInForm() {
    $control = new \SignInFormControl();
    $form = $control['form'];
    $form->onSuccess[] = callback($this, 'signInFormSubmitted');
    // doplnim co budem potrebovat
    return $control;
}

v sablone:

{includeblock "sablona.latte", form => $presenter[signInForm]['form']}
// alebo defaultne
{control signInForm}

Ale neviem vyriesit este poslednu variantu: Ako definovat formular ked nechcem urobit komponentu form class subor, ale si ho chcem nadefinovat priamo v presenteri? Ale chcem aby bol potomkom BaseFormControl. Viete mi poradit ako na to?

Uvazoval som o variante ze si urobim este jednu univerzalnu komponentu ktora bude potomkom BaseFormControl a tuto budem pouzivat v presentri:

class BaseForm extends BaseFormControl {
    protected function createComponentForm() {
        $form = new UI\Form;
        return $form;
    }
}

a v presentru:

protected function createComponentNejakyFormForm() {
    $control = new \BaseForm();
    $form = $control['form'];
    // naplnim
    return $control;
}

Editoval duskohu (12. 11. 2012 16:00)

mildabre
Člen | 62
+
0
-

Nepřemýšlel Jsi někdy o tom mít ve formuláři další nadstavbovou funkcionalitu týkající se tlačítek a potvrzování akcí ?

To co vylepšuješ svým BaseFormControl-em je taková formalita, ale není to žádná přidaná hodnota do funkcionality formuláře.

Já mám takovou vizi – mít v každém formuláři již vestavěné 3 typy tlačítek:

EDIT
DELETE
CLOSE

Tlačítko EDIT by normálně odeslalo data s validací a následným použitím dat.

Tlačítko DELETE by mazalo záznam v tabulce po předchozím potvrzením Javascriptem confirm(…), byl-li by Javascript vypnutý nahradila by ho flashMessage s tlačítky ANO, NE.

Tlačítko CLOSE by zavřelo formulář = přesměrování na url odkud byl formulář zavolán. Formulář by sledoval svoje původní a změněná data a hlídal by odchod z formuláře jak přes tlačítko CLOSE nebo iné tlačítko formuláře (s výjimkou tlačítek EDIT a DELETE), tak i přes jakýkoliv odkaz či jiný formulář (např. přihlašovací) – vyžadoval by Javascriptem confirm() v případě že data formuláře byla změněna. Při vypnutém Javascriptu by toto fungovat nemuselo.

Ono totiž pokud má být aplikace pro uživatele košer – je potřeba každé mazání pečlivě konfirmovat a také hlídat již vyplněná data aby o ně uživatel omylem nepřišel.

Dalším vylepšením formuláře by mohly být např. softValidační pravidla – velmi často je v praxi situace, kdy je potřeba cosi hlídat, ale striktní pravidlo obvykle nevyhoví na 100% a je tam vždy potřeba umožnit uživateli aby pravidla porušil. V tomto případě by formulář pouze upozornil, že některá pole mají možná špatnou hodnotu/chybí a dal by uživateli na vybranou – odelsat či opravit.

Tohle by byly produktivní navýšení hodnoty formulářů, ideálně to pořešit na úrovni celého frameworku. Zajímalo by mne zda mají jiní i tento názor.

Moc dobře si pamatuji na jednu aplikaci s kolegou, kde donekonečna psal kód pro potvrzující podmínky a kód pro upozornění, že uživatel možná něco zapoměl vyplnit, protože tvrdá validace během týdne začala vadit.

Výše nastíněný formulář jsem si pořešil v čistém PHP včetně vizuální indikace chyb přímo ve formuláři a bylo to super – bleskový vývoj aplikace. 90% formulářů v určitém typu aplikací vypadá právě jak je výše popsáno.