Formuláře#
Jádro je app/Core/Forms/ — vlastní třída Form nad Bootstrap formuláři, továrna
a několik vlastních prvků. Formulář se nikdy nepíše přímo v presenteru ani
v komponentě: patří do FormTrait vedle nich.
| Kde formulář žije | Cesta |
|---|---|
| komponenta | XxxComponent/Traits/FormTrait.php |
| presenter | Presenters/Traits/Forms/XxxFormTrait.php |
| admin presenter | totéž, ale kostru dělá createForm() — viz Admin presenter |
Kanonická kostra#
protected function createComponentForm(): Form
{
$form = FormFactory::create();
$form->setMethod('POST');
$form->addProtection();
$form->useDomainTranslator($this->translator, $this->translatorDomain);
$form->setHtmlAttribute('role', 'form');
$form->setHtmlAttribute('class', 'ajax');
$form->addText('name', 'name.label')
->setRequired('name.required')
->setHtmlAttribute('placeholder', 'name.placeholder')
->setOption('description', 'name.description'); // jen klíč!
$form->addSubmit('send', 'send');
$form->onSuccess[] = [$this, 'onSuccess'];
return $form;
}
| Řádek | Proč tam je |
|---|---|
FormFactory::create() |
vrátí App\Core\Forms\Form, ne Nette Form — jinak přijdete o všechny prvky níž |
setMethod('POST') |
vždy explicitně |
addProtection() |
CSRF token, povinně |
useDomainTranslator(…) |
labely jsou pak holé klíče ('name.label'), doménu přidá translator |
role('form') |
přístupnost |
Starý zápis zůstává funkční — jen se nepíše do nového kódu
setTranslator() + setTranslatorDomain() + $t = $form->getTranslateFnc()
funguje dál a v hotových formulářích se neupravuje. Kanonicky se píšou nové.
Co Form umí navíc oproti Nette#
| Metoda | Co udělá |
|---|---|
addEditor($name, $caption) |
textarea s data-editor="full" → plný TinyMCE. Pro administrátorský obsah (stránky, články, e-maily, PDF šablony) |
addSimpleEditor($name, $caption) |
textarea s data-editor="simple" → osekaný editor: tučné, kurzíva, barvy, seznamy, tabulka. Žádné odkazy, obrázky ani zdrojový kód |
addCheckbox($name, $caption) |
plochý checkbox bez bootstrapového obalu (FlatCheckboxInput) |
addCheckboxList($name, $label, $items) |
zaškrtávací seznam, který v popisku volby zachová Html |
addImageUpload($name, $caption, …) |
pole pro jeden obrázek + satelitní pole <name>CurrentId a <name>Delete |
useDomainTranslator($translator, $domain) |
translator s prefixem domény |
Ke každému add*Editor a addImageUpload existuje statická varianta
(addEditorControls(), addImageUploadControls()), která bere kontejner — používá se
uvnitř řádku Multiplieru, kde je jen Nette\Forms\Container bez našich metod.
Do uživatelského obsahu patří addSimpleEditor(), ne addEditor()
Plný editor pustí odkazy, obrázky i vložený zdrojový kód. Kde píše obsah
uživatel (formuláře na frontu, parametry inzerátů), musí být osekaná varianta —
a její whitelist musí zůstat shodný se serverovou sanitizací
(EditorHtmlSanitizer::ALLOWED_HTML). Omezení jen v JavaScriptu se obejde
odesláním požadavku mimo prohlížeč.
Postup: přidávám formulář do komponenty#
FormTraitvedle komponenty, metodacreateComponentForm(): Form.- Kostru podle vzoru výš.
- Delší formulář rozděl do metod
addXxxFields(Form $form); naplnění selectů doprepare*(). onSuccesss typovanou signaturou:
public function onSuccess(Form $form, ArrayHash $values): void
{
$entity = new ContactMessage();
$entity->setEmail($values['email']);
$result = $this->contactMessageManager->save($entity);
if ($result->isSuccess()) {
$this->flashMessage('result.success', 'alert-success');
$form->setValues([], true);
} else {
$this->flashMessage('result.error', 'alert-danger');
}
if ($this->presenter?->isAjax()) {
$this->redrawControl();
}
}
- Flash zprávu pošli holým klíčem — šablona komponenty má translator už
nastavený na doménu komponenty a
{_$flash->message}doménu přidá sama. - Překladové klíče doplň zároveň, ve všech jazycích — viz Překlady.
onSuccess(Form $form, mixed $values) shodí KAŽDÝ submit
Nette z druhého parametru odvozuje, jaký tvar hodnot vyrobit
($this->getValues(Helpers::getSingleType($params[1]))). U mixed z toho vznikne
new ReflectionClass('mixed') → ReflectionException: Class "mixed" does not exist.
Fallback tam žádný není. Netypovaný parametr je taky špatně — dostanete $this
nebo tlačítko místo hodnot. Vždycky ArrayHash $values.
Přesně tohle znemožnilo spustit stavový automat objednávky z administrace, a nikdo
si toho měsíce nevšiml: harness volal Api endpoint přímo a formulář nesubmitoval.
Kontrola: grep -rn 'onSuccess\[\] = function' app | grep mixed.
Postup: vykresluji formulář v Latte#
{form editForm}
<div class="form-group">
{label name /}
{input name}
<span class="errors" n:ifcontent>{inputError name}</span>
<small class="form-text text-muted" n:ifcontent>
{$form['name']->getOption('description') ? _($form['name']->getOption('description')) : ''}
</small>
</div>
<label>
{input active:}
{label active}{_$form['active']->caption}{/label}
</label>
{/form}
| Prvek | Zápis |
|---|---|
| chyba pole | inline {inputError name} u toho pole |
| checkbox | {input active:} — dvojtečka je povinná |
| popisek checkboxu | {label active}{_$form['active']->caption}{/label}, nikdy {label active /} |
| description | v PHP jen klíč, v Latte se překládá přes _() |
{control $form errors} v ručně vykresleném formuláři nepoužívejte
Souhrnný výpis nahoře znamená, že uživatel vidí „Vyplňte název“, ale nevidí které pole to je — u formuláře s jazykovými záložkami je to chybné pole často schované v neaktivní záložce.
Checkbox si vykresluje vlastní popisek
{label active /} (se samouzavřením) vypíše popisek podruhé. Proto ta varianta
s {_…->caption} uvnitř {label}.
n:if na poli formuláře SMAŽE jeho hodnotu
Nevykreslené pole se neodešle a uložení ho zapíše jako prázdné. Platí to i pro
addHidden(). Chcete-li pole podmínit, řešte to při sestavení formuláře
(nepřidat ho), ne v šabloně. Data zmizí bez chybové hlášky.
Postup: opakované skupiny polí (Multiplier)#
- Předvyplnění dělej přes
setDefaults()na kontejneru, ne nastavením hodnot jednotlivým prvkům. - Na už odeslaném formuláři
addCopy(null, $defaults)nefunguje — uvnitř voláContainer::setDefaults(), a ten na odeslaném formuláři nastaví jen zakázaná pole. Kopie přijde prázdná. Správně:
$copy = $outerMultiplier->addCopy(); // bez defaults
$copy->setValues($row); // přímé kontrolky — funguje vždy
- Vnořený Multiplier na odeslaném formuláři ignoruje programově dodané hodnoty —
staví kopie z odeslaných dat, ne z hodnot. Nový kontejner v odeslaných datech není,
takže vnitřní kopie vzniknou nulové. Musíte je přidat explicitně
(
getComponent($i, false) ?? $nested->addCopy($i)a paksetValues()). - Řádek potřebuje vlastní třídu na obalu — bez ní se prvky rozjedou v rozvržení.
setValidationScope([]) vyprázdní getValues()
Kontrolky mimo validační rozsah submitteru Nette označí jako omitted
a getValues() je přeskočí. U Multiplieru to vypadá obzvlášť zákeřně:
getValues()->items vrátí [{}] — řádek existuje, ale je prázdný.
Pomocná AJAX tlačítka (recalculate, „přepočítat“, překreslení Pickeru) mají
prázdný rozsah schválně, aby neblokovala na required, když admin teprve
skládá formulář. Jejich obsluha proto musí číst kontrolky napřímo —
$multiplier->getContainers() plus $control->getValue(). Kvůli tomuhle
v e-shopu nikdy nefungovalo „Přepočítat“ ani předvyplnění dopravy: chybová hláška
se vykreslovala do snippetu, který se nepřekresloval, takže selhání bylo němé.
Obsluha AJAX tlačítka musí překreslit i snippet se svou chybovou hláškou
Jinak tlačítko „nic nedělá“ — a je jedno, jestli chyba nastala, nebo ne.
Postup: nahrávání souborů#
Sdílený UploaderDropzone nahraje soubory předem AJAX signálem do dočasné složky
klíčované tokenem; finální submit je odtud přečte podle skrytého pole dropzoneToken
a skrytých položek uploaderItems[…].
- Token vlož do formuláře jako skryté pole.
- Na POST překreslení token NEGENERUJ znovu — přečti ho z odeslaných dat
a nastav přes
setToken($posted).prepareTempDir(forceNew: true)by vyrobil novou prázdnou složku. - Novou složku zakládej jen na čerstvém GET (v komponentě se to pozná podle
getSignal() === null).
Bez převzetí tokenu zmizí rozpracované nahrávání při první validační chybě
Scénář: uživatel nahraje obrázek → odešle formulář → validace selže na něčem jiném
→ snippet s galerií se překreslí → serverové HTML zná jen soubory z databáze, ne
ty v dočasné složce → skryté položky uploaderItems zmizí. Uživatel chybu opraví,
odešle znovu, a záznam se uloží bez obrázku — přestože soubor pořád leží
v dočasné složce. Šťastná cesta bez chyby funguje, takže se to odhalí až při
ladění, kdy validace padá často.
Uložení nahradí celý seznam příloh
Nenačte-li se pole s nahráváním, uloží se prázdný seznam a přílohy zmizí. Je to tatáž mechanika jako u kolekcí — Hydrátory, oddíl o ukládání objektového grafu.
Postup: formulář se odeslal, ale nic se neuložilo#
Od nejlevnějšího:
- Přišel flash? Když ne, podívej se, jestli se nepřesměrovává přes
redirectUrl()— ten flash zahodí a akce vypadá, že neproběhla. - Které tlačítko formulář odeslalo? V administraci ukládají jen
saveaupdate; vlastní submit proběhne, ale neuloží. - Má obsluha
ArrayHash $values? Viz varování omixedvýš. - Nemá tlačítko
setValidationScope([])? Pak jsou hodnoty prázdné. - Vykresluje šablona všechna pole? Nevykreslené pole se uloží jako prázdné.
- Vrátil manager
isSuccess()? Neúspěch bez flash zprávy je němý. - Teprve pak koukej do databáze.
Formulářová vrstva potřebuje aspoň jeden test, který formulář SKUTEČNĚ odešle
Sestavení formuláře ani volání Api endpointu pod ním tuhle třídu chyb nezachytí — obojí projde, i když formulář nejde odeslat. Chytí to jen POST nebo živý proklik prohlížečem.
Po změně struktury formuláře načtěte stránku dvakrát
První načtení ještě jede podle staré podoby z mezipaměti.
Dynamické parametry#
Vedle ručně psaných formulářů existuje systém parametrů
(app/Core/Form/Parameters/): pole se skládá podle konfigurace v databázi, ne podle
kódu. Používá ho inzerce, katalog firem i e-shop — každá kategorie má jinou sadu polí
a ta se dá měnit z administrace bez zásahu do kódu.
Chování v administraci popisuje Kategorie a parametry.
Kam sáhnout#
| Chci | Kde |
|---|---|
| vlastní prvky a helpery | app/Core/Forms/Form.php |
| továrnu formuláře | app/Core/Forms/FormFactory.php |
| vlastní inputy | app/Core/Forms/Inputs/ |
| kostru admin formuláře | app/Core/Base/Admin/BasePresenter.php → createForm() |
| sdílený uploader | app/UI/Admin/Base/Components/UploaderDropzoneComponent/ |
| sanitizaci editoru | app/Core/Utils/Helpers/EditorHtmlSanitizer.php |
| dynamické parametry | app/Core/Form/Parameters/ |
| závaznou konvenci | skills/code-standard/cs-conv-forms/, skills/code-standard/cs-form-setup/ |
Navazující kapitoly: Admin presenter · Picker · Šablony Latte · Překlady · Konvence kódu