Přeskočit obsah
V
Pro vývojáře
Architektura, konvence, jádro systému a bezpečnost
UI vrstva / Formuláře

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#

  1. FormTrait vedle komponenty, metoda createComponentForm(): Form.
  2. Kostru podle vzoru výš.
  3. Delší formulář rozděl do metod addXxxFields(Form $form); naplnění selectů do prepare*().
  4. onSuccess s 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();
    }
}
  1. 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.
  2. 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)#

  1. Předvyplnění dělej přes setDefaults() na kontejneru, ne nastavením hodnot jednotlivým prvkům.
  2. 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
  1. 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 pak setValues()).
  2. Řá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[…].

  1. Token vlož do formuláře jako skryté pole.
  2. 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.
  3. 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:

  1. 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.
  2. Které tlačítko formulář odeslalo? V administraci ukládají jen save a update; vlastní submit proběhne, ale neuloží.
  3. Má obsluha ArrayHash $values? Viz varování o mixed výš.
  4. Nemá tlačítko setValidationScope([])? Pak jsou hodnoty prázdné.
  5. Vykresluje šablona všechna pole? Nevykreslené pole se uloží jako prázdné.
  6. Vrátil manager isSuccess()? Neúspěch bez flash zprávy je němý.
  7. 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.phpcreateForm()
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