Závislé komponenty – lze jednokrokově měnit strukturu formuláře podle hodnoty?

Toanir
Člen | 57
+
0
-

Čus vespolek, mám problém.

Problém

Snažím se vymyslet formulář, kterej se bude přiohýbat vybraným hodnotám ostatních prvků. Potřeboval bych něco ve smyslu závislých selectboxů, na který jsem tu našel pár témat ale z žádnýho jsem nevykoukal, co potřebuju. Chtěl bych být o krok generičtější, tj například:


// pokud $container['verdict']->value === 'accept', potom
$container['additional'] = Container([
  'commendation' => TextInput()
]);

// pokud $container['verdict']->value === 'postpone', potom
$container['additional'] = Container([
  'postponeDays' => NumberInput()->required(),
  'explanation' => TextArea(),
])

a případně další varianty. Zbytek funkcionality je REST-friendly ale nechce se mi ještě pouštět do klientský aplikace a chtěl bych mít chytrej formulář. Povedlo se mi formulář překreslit v rámci signálu ale neporadím si s odesíláním.

Řešil jste někdo takhle dynamickej formulář? Případně působí to na vás jako řešitelný problém pomocí Nette formulářů v rámci rozumnýho úsilí? Čím víc si s tím hraju, tím víc se chystám zkusit malou client-side appku.

Problém -v

Zatím jsem zkoušel:

  • v handle jen říct, na jaký typ chci měnit a překreslit formulář kvůli tomu jsem ztratil zbytek hodnot (předvyplní se uložený hodnoty)
  • v handle jen překreslit konkrétní inputy pro additional ale bylo to strašně moc kódu a pak jsem narazil na problém s odesíláním, (loadHttpData pracuje s formulářem v původní podobě)
  • v handle přijímám všechny hodnoty, nastavuju přes setValues a překresluju celý formulář (stejný problém jako posledně ale za mnohem míň kódu)

Proteď si od toho dávám oraz a prosím vás o kritiku a nápady (Jsem připravený i na „Vykašli se na to a luskni tam Angular“ :D)

class ResolutionContainer extends Nette\Forms\Container {

    private $verdictAdditionalDataControlFactory;

    public function __construct(VerdictAdditionalDataControlFactory $verdictAdditionalDataControlFactory) {
        $this->verdictAdditionalDataControlFactory = $verdictAdditionalDataControlFactory;

        $possibleVerdicts = $verdictAdditionalDataControlFactory->getPossibleVerdicts();
        reset($possibleVerdicts);
        $defaultVerdict = key($possibleVerdicts);
        $this['verdict'] = (new SelectBox())
            ->setItems($verdictAdditionalDataControlFactory->getPossibleVerdicts())
            ->setDefault($defaultVerdict);

        $this['additional'] = $this->verdictAdditionalDataControlFactory->create($defaultVerdict);
    }

    public function setValues($data, bool $erase = false)
    {
        $currentVerdict = $this['verdict']->getValue();
        $verdict = $data['verdict'] ?? $currentVerdict;

        if ($currentVerdict !== $verdict) {
            unset($this['additional']);
            $this['additional'] = $this->verdictAdditionalDataControlFactory->create($verdict);
        }
        if (!isset($data['additional'])) {
            $data['additional'] = [];
        }
    }
}

A ve formuláři, kde to používám mám handle, kterej přijme JavaScriptem vyzobaný hodnoty:

let formData = new FormData(form);
let values = {};
for ([name, value] of formData.entries()) {
    let dotName = convertHttpNameToDots(name);
    _.set(values, dotName, value);
}
return values;

který pošle POSTem přes signál do setValues(). V tom signálu potom jen překreslím formulář a vrátím ho jako TextResponse, kde to překreslený formulář.

Uvědomuju si, že to je špatný na hned několika úrovních:

  • hlavně to obchází loadHttpData :(,
  • používá to interní setValues
  • v podstatě to dělá to, co snippety
  • (bludišťáka těm, kdo najdou další přešlapy :D)

Problém s odesíláním je, že ve formuláři mám defaultní variantu pro ‚additionals‘, kterou vyrobím s kontejnerem, a ta si při odeslání formuláře jen načítá hodnoty… což dává smysl, ale snažím se dokousat k něčemu jako je contributte/forms-multiplier.

F.Vesely
Člen | 368
+
+1
-
Toanir
Člen | 57
+
0
-

Dík za radu, myslím si že problém dependent selectboxů je typově jiný problém. Dependent select box vnímám jako problém „Tady mám konkrétní Select (Formulářový Control) a jeho vnitřní struktura (items) závisí na 1-N ostatních prvcích“

Problém, který řeším je „Mám selectBox a na základě jeho hodnoty je závislých 1-N prvků“.
Pro jednoduchost zkouším tyhle prvky dát do Forms\Container abych mohl použít $form['additional'] = $factory->createByType('additionalType.pizza');.

Myslím si že by to bylo proveditelný, kdyby si jednotlivý kontrolky nevytahovaly z formuláře jejich HTTP data samy ale kdyby jim to formulář servíroval, třeba v takovýmhle smyslu:

Form::initHttpData // volá se když formulář zapadne do komponentovýho modelu => známe celý jméno jednotlivejch komponent
  $data = $this->ge
  foreach $this->components as $k => $c
  - pokud $c je Container, $c->setHttpData(..)
  - pokud $c je BaseControl, $c->setHttpData(..)

Tak by se dal vytvořit kontejner který si tohle rozdělení a inicializaci ošéfuje sám.

m.brecher
Generous Backer | 725
+
-1
-

Také jsem se snažil o dynamičtější formulář, kdy by strukturu formuláře bylo možné ovlivnit kliknutím na tlačítko ve formuláři. To ale v Nette formulářích je velmi obtížné provést. Strukturu formuláře totiž definuje factory metoda nebo třída a dokud se formulář nevytvoří a nevrátí, není možné číst data ve formuláři. Tohle jde sice nějak obejít přes httpRequest, nebo $_POST, ale je to v podstatě pořád jenom obcházení určité koncepce nette formulářů, které jsou z podstaty statické. Také jsem tudy zkoušel jít, ale bylo s tím strašně moc práce a žádné znovupoužitelné řešení.

Možná by to šlo nějak obejít tím, že by se formulář zabalil do nějaké komponenty, která by tu chybějící funkcionalitu nějak dodala, ale obávám se, že je to na dlouhé hodiny a výsledek značně nejistý.

Představuju si to tak, že obalující komponenta doplní do formuláře jeden prvek hidden s nějakým unikátním jménem např. __form__state a po submitu ještě před sestavením prvků do formuláře odečte pomocí httpRequestu data __form__state a ty pak použije při buildu formuláře. Pokud uživatel v prohlížeči přidal tlačítkem nějaký formulářový element, nebo skupinu elementů (např. položku faktury) a do nich zapsal data, tak by se dynamicky přidané prvky zapsaly do __form__state, a při buildu formuláře ve factory by se dynamicky přidané prvky do formuláře díky znalosti __form__state mohly vykreslit a data do nich uživatelem zapsaná by nette do těch prvků umístilo.

Třeba by to takhle šlo nějak obejít, toužím v podstatě o možnost přidávat dynamicky do formuláře položky faktury, další obrázky, soubory ke stažení a pod… to vše v jednom formuláři a je mě jedno, jestli se element/skupina elementů přidá javascriptem nebo na serveru v php. Pro uživatele je totiž komfortnější práce v kontextu informací, nikoliv jenom s izolovaným výsekem dat.

Editoval m.brecher (5. 10. 2021 15:28)