Vlastní renderer a šablona – jak invalidovat snippet?
- SvvimX
- Člen | 65
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
Š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ář
Control
em.
EDIT: Odpíchnout se můžeš od tohoto vlákna.
Editoval jiri.pudil (30. 8. 2013 14:31)
- Vojtěch Dobeš
- Gold Partner | 1316
Zdravíčko, pár věcí:
- Tohle
url: '{link {$link}!}',
je špatně, stačíurl: '{link $link!}',
. - 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 odUI\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
@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
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.
- Vojtěch Dobeš
- Gold Partner | 1316
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
upravil jsem si tedy kódy, podle příkladů, mám formuláře v komponentách a mám otázku a problém:
- 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…?
- 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
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
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
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
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
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.
- co říkáš na to předání $this (presenteru) jako $parent do komponenty?
- 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ě?
- 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 :-)