Vlastní renderer a šablona – jak invalidovat snippet?

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

Ahoj,

využívám https://forum.nette.org/…stni-sablonu#… tedy vlastní renderer pro formuláře. Pokud se najde šablona formuláře, použije se, jinak se využije default (respektive twitter bootstrap addon). Mám tedy formulář:

class WebsiteForm extends BaseForm {


    public function __construct( array $companies, \Nette\ComponentModel\IContainer $parent = NULL, $name = NULL ) {

        parent::__construct( $parent, $name );

        $this -> setMethod ( "POST" );

        $this -> addSelect( 'company', __( 'Company'), $companies )
		->setTranslator(NULL)
		;

	$this -> addMultiSelect( 'users', "Assign users", array() )
		-> setOption ( "description", "For select more or none use CTRL" )
		;

	$this -> addSubmit ( 'submit', 'Save' );

    }

}

K němu šablonu:

{extends @layout.latte}

{block form}
    {form "websiteForm", class=>'form-horizontal'}
	  {include #errors, form=>$form}




		{label company, class=>'col-md-2' /}

		{input company, class=>'form-control'}

		{label users, class=>'col-md-2' /}

		{snippet usersSelect}
			{input users, class=>'form-control'}
		{/snippet}

		{input submit, class=>'btn btn-success'}

    {/form}


    <script>
    {include #jsCallback, formname => "websiteForm", input => company, link => companyChange}
    </script>

    {define #jsCallback}

	$('#{$_control[$formname][$input]->htmlId}').on('change', function() {
	    $.nette.ajax({
		type: 'GET',
		url: '{link {$link}!}',
		data: {
		    'value': $(this).val(),
		}
	    });
	});

    {/define}

{/block}

a v presenteru

protected function createComponentWebsiteForm ( $name = NULL )
    {
	$form = new WebsiteForm ( $companies );
	$form->onSuccess[] = $this -> processWebsiteFormSubmitted;

	return $form;
    }

/**
     * Load values to user multi select
     * @param int
     */
    public function handleCompanyChange ( $value )
    {

        if ( $value ) {
	    $users = array ( );

	    $userEntities = $this ->getService( "userManager" ) -> getBy (
							    array ( "company" => $value ),
							    array ( "surname" => "ASC" )
						    );
	    foreach ( $userEntities as $entity )
		$users [ $entity -> id ] = $entity -> name . " " . $entity -> surname;

            $this['websiteForm']['users']->setItems($users);

        } else {
            $this['websiteForm']['users']->setItems(array());
        }

	$this->invalidateControl('usersSelect');
    }

V šabloně presenteru je

{block title}{_"Add new website"}{/block}

{block content}

    <h2>{_}Add new website{/_}</h2>

    {control websiteForm}

{/block}

Při změně selectu se společností se zavolá metoda handleCompanyChange (testováno) ale snipet se neinvaliduje, našel jsem vlákno https://forum.nette.org/…vana-sablona ale jestli dobře chápu, mělo by to být již vyřešeno, pletu se, nebo mám chybu někde jinde?

Díky za rady

Editoval SvvimX (30. 8. 2013 13:55)

jiri.pudil
Nette Blogger | 1029
+
0
-

Šablona presenteru si makro control přeloží na zavolání metody render() dané komponenty. To, jak se komponenta vykreslí a že obsahuje nějaké snippety, už presenter neví (resp. je mu to úplně jedno). Řešením je invalidovat snippet v komponentě. Problém je ale v tom, že Form nedisponuje metodou invalidateControl(). Takže se nevyhneš tomu, obalit si formulář Controlem.

EDIT: Odpíchnout se můžeš od tohoto vlákna.

Editoval jiri.pudil (30. 8. 2013 14:31)

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

Zdravíčko, pár věcí:

  1. Tohle url: '{link {$link}!}', je špatně, stačí url: '{link $link!}',.
  2. Snippet je v šabloně formuláře, tzn. presenter ho nemůže vidět. Bohužel formulář je zase taková komponenta, na které nelze snippety invalidovat. Proto je správným řešením nedědit od UI\Form, ale klasicky od UI\Control. Vznikne mnohem hezčí kód :)
class WebsiteForm extends UI\Control
{

	/** @var array */
	private $companies;



	/**
	 * @param  array
	 */
	public function __construct(array $companies)
	{
		parent::__construct();

		$this->companies = $companies;
	}



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

		$form->setMethod('POST');

		$form->addSelect('company', __('Company'), $this->companies)
			->setTranslator(NULL);

		$form->addMultiSelect('users', 'Assign users', array())
			->setOption ('description', 'For select more or none use CTRL');

		$form->addSubmit('submit', 'Save');

		return $form;
	}



	public function render()
	{
		$this->template->setFile( ... cesta k šabloně );
		$this->template->_form = $this->template->form = $this['form']
		$this->template->render();
	}

}
{form form class=>'form-horizontal'}
	{include #errors, form=>$form}

	{label company, class=>'col-md-2' /}

	{input company, class=>'form-control'}

	{label users, class=>'col-md-2' /}

	{snippet usersSelect}
		{input users, class=>'form-control'}
	{/snippet}

	{input submit, class=>'btn btn-success'}
{/form}

<script>
	{include #jsCallback, formname => form, input => company, link => companyChange}
</script>

{define #jsCallback}
	$('#{$_control[$formname][$input]->htmlId}').on('change', function() {
		$.nette.ajax({
			type: 'GET',
			url: '{plink $link!}',
			data: {
				'value': $(this).val()
			}
		});
	});
{/define}

Presenter, ve zpracování formuláře:

$this['websiteForm']->invalidateControl('usersSelect');

A v továrničce:

$form['form']->onSuccess[] = $this->processWebsiteFormSubmitted;

Editoval vojtech.dobes (30. 8. 2013 14:57)

SvvimX
Člen | 65
+
0
-

@vojtech.dobes:
ad 1) sem opsal :) url: '{link {$link}!}' https://blog.nette.org/…-and-pure-js
ad 2) a mě se tak líbilo, že form mi zůstal formem a ne komponentou.. Vycházel jsem z https://forum.nette.org/…stni-sablonu#… kde Šaman píše: „Variantu s komponentou, která obsahuje $form jsem úspěšně používal, ale není to úplně čisté – já chci přiřadit šablonu (resp. renderer) formuláři a ne vytvářet novou komponentu okolo formuláře jenom proto, že ona už se šablonou umí pracovat..“
Přišlo mi víc správné, nechat form jako form, ale když form neumí snippety, tak to asi jinak nepůjde.. Jak jinak taky udělat 2 závislé selecty, přitom mít kód formuláře stranou od presenteru a zachovat možnost – v šabloně presenteru mít control form a přitom využívat někdy default renderer někdy vlastní šablonu formuláře atd..

Skoro si myslím, že nejsnažší by bylo aby formuláře měli možnost invalidovat snippety, proč to vlastně nejde?

Díky za radu, vyzkouším dávat form do komponent :(

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

Proč to nejde? Protože to často není potřeba :). Podle mě jde jen o Šamanův pocit. Naopak při využití UI\Control je kód elegantnější, jasně čitelný (žádné hackování do konstruktoru nebo attached metody), a ano, šablona i snippety jsou hned po ruce a nemusí se složitě doplňovat.

Při vytvoření inteligentního BaseFormControl se bude kód lišit jen v tom, že se formulář nenadefinuje v konstruktoru či attached metodě, ale v klasické srozumitelné továrničce.

SvvimX
Člen | 65
+
0
-

jasně, ale v presenteru musíš v továrničce vytvořit
$control = new WebsiteForm;
$control[‚form‘] → …

ale ok, nic jiného mi stejně nezbyde. A když už to takle dělám tak se zeptám, co obsluha odeslání formuláře přímo v komponentě? Ano nebo ponechat v presenteru?

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

To je druhý krok, kterým jsem to nechtěl komplikovat – nejlepší bude celý ten signál přesunout do té komponenty (opět, při dědění formuláře by to nešlo, ten takovéto signály nezná). Presenter se zkrátí, komponenta si bude invalidovat svůj snippet atd… výsledek refactoringu tě, věřím, potěší :).

SvvimX
Člen | 65
+
0
-

upravil jsem si tedy kódy, podle příkladů, mám formuláře v komponentách a mám otázku a problém:

  1. potřebuji v komponentě přístup k presenteru – nebo lépe: potřebuji dostat getService(translator) a další managery (služby). Ale $this → presenter tvrdí, že komponenta '' není připojena k presetneru…?
  2. v komponentě mám metodu pro zpracování signálu
/**
     * Load values to user multi select
     * @param int
     */
    public function handleCompanyChange ( $value ) {

Ale value mi do ní nepřijde

<script>
    {include #jsCallback, formname => $form->name, input => 'company', link => 'CompanyChange' }
    </script>

    {define #jsCallback}

	$('#{$_control[$formname][$input]->htmlId}').on('change', function() {
	    $.nette.ajax({
		type: 'GET',
		url: '{link $link}',
		data: {
		    'value': 2
		}
	    });
	});

    {/define}
SvvimX
Člen | 65
+
0
-

kromě přesunu handle signálu (což jsem udělal) jsem se spíše ptal – co tam přesunout celou obsluhu odeslání, tedy:

public function processWebsiteFormSubmitted ( WebsiteForm $form )
    {

ta je zatím v presenteru, protože v něm se vytváří komponenta a nastavuje se jejímu formuláři onSuccess

protected function createComponentWebsiteForm ( $name = NULL )
    {
...
$form = new WebsiteForm ( $companies, $users );
	$form['form']->onSuccess[] = $this -> processWebsiteFormSubmitted;

	return $form;
    }
Vojtěch Dobeš
Gold Partner | 1316
+
0
-

Jasně, přesunul bych i zpracování formuláře…

Ad ta službu – předej si ji do komponenty v konstruktoru, jako klasickou závislost.

Ten parametr se nyní bude jmenovat websiteForm-value, myslím. Zkus si pro jistotu vygenerovat ten odkaz někam do HTML, a uvidíš, jak se ten parametr jmenuje.

Editoval vojtech.dobes (30. 8. 2013 17:54)

SvvimX
Člen | 65
+
0
-

ok předám si ji v konstruktoru, i když trochu magična bych ocenil :)

vygeneroval jsem si pomocí {link $link, 2}, jmenuje se to websiteForm-value, nicméně v pohodě to spadne do value parametru funkce. Ale

url: '{link $link}',
		data: {
		    'value': 2
		}

ale value (nyní natvrdo 2, jiank tam má být $(this).val()) se mění javascriptem, podle toho, co uživatel vybral v selectu, takže nevstupuje přímo v makru do URL, posílá se přes data ajaxem, a zřejmě nepropadne do parametru funkce, nepracuje se s tím potom nějak jinak? S ajaxem v Nette začínám uplně … :)

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

Omlouvám se, byl jsem zkratkovitý: právě protože se nyní ten parametr vygeneruje jako websiteForm-value, musíš jej tak i uvést ve svém javascriptu.

url: '{link $link}',
data: {
	'websiteForm-value': 2
}

Editoval vojtech.dobes (30. 8. 2013 18:08)

SvvimX
Člen | 65
+
0
-

tak to mi vubec nedošlo, že jde o ten javascript a ne parametr funkce. Super, tak mi to běhá, nějak.

ukážu tady kódy, mohl bys mi ještě říct, jestli je to ok?

Presenter:

protected function createComponentWebsiteForm ( $name = NULL )
    {
	$form = new WebsiteForm ( $this ); // zde do komponenty jako parent predam sebe - tim v ni ziskam pristup k getService, user, translatoru a dalsim vecem
	return $form;
    }

Komponenta

<?php



use	Nella\Forms\Form,
	Nette\ComponentModel\IContainer,
	PERUS\policajt\Exception\DatabaseException
	;

class WebsiteForm extends BaseFormControl {

    /** @var array */
    protected $companies;

    /** @var array */
    protected $users;

    /**
     *
     * @param IContainer $parent
     * @param type $name
     */
    public function __construct( IContainer $parent = NULL, $name = NULL) {
	parent::__construct( $parent, $name);

	$companies = array ();
	$companyID = NULL;
	// only SUPERADMIN can change user company
	if ( $this -> user ->isInRole( SUPERADMIN ) ) {
	    $companyEntity = $this -> getService("companyManager") -> getAll ( array ( "firm" => "ASC" ) );
	    foreach ( $companyEntity as $company )
		$companies [ $company -> id ] = $company -> firm;
	    $companyID = key( $companies );
	} else {
	    $companyEntity = $this -> getService("companyManager") -> getOneBy ( array ( "id" => $this -> user -> identity -> company_id ) );
	    $companies [  $companyEntity -> id ] = $companyEntity -> firm;
	    $companyID = $this -> user -> identity -> company_id;
	}

	$users = array ();
	$usersEntity = $this -> getService ( "userManager" ) -> getBy ( array ( "company" => $companyID ),
									    array ( "surname" => "ASC" ) );
	foreach ( $usersEntity as $entity )
	    $users [ $entity -> id ] = $entity -> name . " " . $entity -> surname;

	$this -> companies = $companies;
	$this -> users = $users;
    }

    /**
     * Load values to user multi select
     * @param int
     */
    public function handleCompanyChange ( $value ) {

	if ( $value ) {
	    $users = array (  );
            $usersEntity = $this ->getService( "userManager" ) -> getBy ( array ( "company" => $value ),
									    array ( "surname" => "ASC" ) );
	    foreach ( $usersEntity as $entity )
		$users [ $entity -> id ] = $entity -> name . " " . $entity -> surname;
            $this['form']['users']->setItems($users);

        } else {
            $this['form']['users']->setItems(array());
        }

	$this->invalidateControl('usersSelect');
    }

    /**
     *
     * @param type $name
     * @return \PERUS\policajt\Form\BaseForm
     */
    protected function createComponentForm ( $name = NULL ) {

	$form = new BaseForm ( $this -> translator );

	$form -> setMethod ( "POST" );

        $form -> addText( 'title', 'Title' )
                -> setRequired ( 'Please enter %label' )
                ;

        $form -> addText( 'url', 'Url' )
                -> setRequired ( 'Please enter %label' )
		;

	$form -> addSelect( 'company', __( 'Company'), $this -> companies )
		->setTranslator(NULL)
		;

	$form -> addMultiSelect( 'users', "Assign users", $this -> users )
		-> setOption ( "description", "For select more or none use CTRL" )
		;


        $form ->addCheckbox( "active", "Active" );


        $form -> addSubmit ( 'submit', 'Save' );

	$form->onSuccess[] = $this -> processWebsiteFormSubmitted;

	return $form;
    }



    /**
     * Process Form, create or update entity depends on persistent id
     *
     * @param WebsiteForm $form
     * @return void
     */
    public function processWebsiteFormSubmitted ( BaseForm $form )
    {
	$values = $form->getHttpData();
        try {

	    if ( $this -> parent -> id )
		$this -> getService( "websiteManager" ) ->update ( $this -> parent -> id, $values ) ;
	    else
		$this -> getService( "websiteManager" ) -> create ( $values ) ;

	} catch ( DatabaseException $e ) {
	    error_log( $e -> getMessage() );
	    $form -> addError ( "Website can not be " . ( $this -> parent ->  id ? "updated" : "created" ) );
	    return;
	}

	$this -> flashMessage ( "Website was sucessfully " . ( $this -> parent ->  id ? "updated" : "created" ), "success" );

	$this -> parent -> id = NULL;
	$this -> parent -> redirect( "Website:" );
    }

}

k tomu je ještě BaseForm – dostane a nastavi translator, nastavi renderer na vlastni
a BaseFormControl, který jen vezme $parent (což je presenter), vytáhne translator, user a podobně a pořeší šablony komponenty.

  1. co říkáš na to předání $this (presenteru) jako $parent do komponenty?
  2. co konstruktor komponenty, který tahá z DB spoelčnosti a uživatele a ukládá si je pro pozdější použití formuláře? Není to špatně?
  3. Kde nastavovat default data, když budu editovat – v presenteru se vyvolá actionEdit, ten vytáhne z DB info o záznamu který edituji a předá ho do komponenty do formuláře:
$this [ 'websiteForm' ] [ 'form' ] -> setDefaults ( $defValue );

ok?

Jinak díky moc za rady, máš u mě nejméně pivo :-)