RFC přidat metodu setReadonly() do Forms\Controls\BaseControl

m.brecher
Generous Backer | 758
+
+5
-

Ahoj,

Běžně požadovanou funkcí ve formulářích je použití prvku, který zobrazí hodnotu ale nejde editovat (obdoba readonly property objektu).

Typické použití je při vytváření nového záznamu předat do formuláře výchozí data, která se mají uživateli zobrazit, bez možnosti editace a po odeslání formuláře uložit do databáze. Existující metodu Nette formulářů setDisabled() sice lze použít pro zobrazení needitovatelné hodnoty, ale hodnota se na server neodesílá, což v řadě případů nevyhovuje.

HTML nabízí atribut readonly, který ale nemá optimální funkci (může získat focus, což je matoucí) a nelze ho použít pro některé prvky (select nebo checkbox).

Ideální tedy je použít pro readonly prvky formulářů setDisabled(), který by ale odesílal hodnotu na server.

Nette formuláře nabízejí možnost kombinací dvou metod tuto funkci readonly k disablovanému prvku přidat:

// stávající řešení readonly:

$form->addText('name', 'Caption')->setDisabled()->setOmitted(false);

Tato kombinace je uvedena v dokumentaci zde: https://doc.nette.org/…rms/controls#…

Funkce je přesně taková, jakou potřebujeme, ale kód je obtížně čitelný a obtížně se pamatuje. Přitom každý by očekával, že Nette nabídne vedle setDisabled() i metodu setReadonly().

Navrhuji proto doplnit do třídy Nette\Forms\Controls\BaseControl tuto metodu následujícím způsobem:

public function setReadonly(bool $value = true): static
{
    $this->disabled = $value;
    $this->omitted = $value ? false : null;
    return $this;
}

Kód ve formulářích bude dobře čitelný:

// disablovaný prvek:

$form->addText('name', 'Caption')->setDisabled();

// readonly prvek:

$form->addText('name', 'Caption')->setReadonly();

Funguje i vypnutí tohoto nastavení:

// nastavení readonly:

$form->addText('name', 'Caption')->setReadonly();

// vypnutí readonly:

$form->addText('name', 'Caption')->setReadonly(false);

Dalším benefitem je našeptávání v IDE.

Zde je příslušný PR do repozitáře nette/forms: https://github.com/…rms/pull/324

Editoval m.brecher (29. 3. 1:51)

MajklNajt
Člen | 471
+
0
-

OT: kedy potrebuješ do databáze posielať dáta, ktoré tam už sú?

m.brecher
Generous Backer | 758
+
+1
-

@MajklNajt

kedy potrebuješ do databáze posielať dáta, ktoré tam už sú?

Např. když vytvářím nový článek, potřebuji uložit do created_at aktuální datum, a potřebuji datum zobrazit + blokovat editaci. U nového záznamu datum v databázi ještě není.

$form->addDate('created_at', 'Datum založení')
    ->setReadonly();

// .....


$form['created_at']->setDefaultValue(DateTime:from('now'));

Editoval m.brecher (29. 3. 18:06)

David Grudl
Nette Core | 8139
+
0
-

Mě se to pojmenování setReadOnly nelíbí, protože to nevystihnuje podstatu, tedy že rozdíl mezi readonly a disabled je v tom, že se položky neobjeví ve vrácených datech. Tady situaci komplikuje fakt, že disabled položky se v datech objeví, tedy Nette je tam předává i když je prohlížeče neposílají. Je to proto, že na fóru vzniklo milion dotazů „proč po odeslání nevidím disabled data“, jelikož moře programátorů nevědělo, jak disabled atribut funguje.

Ta metoda by se měla jmenovat spíš nějak jako setUnsent(), ale to je taky nesrozumitelné.

m.brecher
Generous Backer | 758
+
0
-

@DavidGrudl

readonly

Ideální název pro prvek fomuláře, který nelze editovat, ale data zobrazuje a odesílá. Klíčové slovo readonly je takto univerzálně používáno napříč programovacími jazyky. I PHP readonly properties nelze editovat, ale data poskytují.

disabled

HTML norma definuje, že atribut disabled blokuje editaci a funkci formulářového prvku. Data se nesmazávají ale naopak zobrazují. Data se ale neodesílají a zda se data v disablovaném prvku po submitu zobrazí záleží ne na normě html, ale na způsobu zpracování formuláře na serveru. Pokud po submitu do formuláře vykreslíme data, která formulář odeslal, tak z disablovaných prvků data zmizí – protože je formulář neodeslal. Takové chování nedává smysl – data vidím a po odeslání zmizí. Můžeme ale data po submitu u disablovaných prvků obnovit z default values a potom data nezmizí. To je žádoucí a užitečná funkce. V současné implementaci setDisabled() v nette formulářích to přesně takto je a je to tak správně.

Můžeme si vybrat ze dvou variant, které se obě hodí:

a) setDisabled() – data vidím, data nezmizí, nelze editovat, data se na server NEODEŠLOU
b) setReadonly() – data vidím, data nezmizí, nelze editovat, data se na server ODEŠLOU

Názvy mě připadají výstižné, v souladu s html normou i s všeobecným povědomím o disabled a readonly

nedostatky disabled v nette

V Nette formulářích se nastavením setDisabled() aktuální default value prvku smaže a data se tedy musí do prvku dodat později. To je v rozporu s normou html, která neříká nic o smazání dat, pouze se blokuje funkce prvku, blokuje se focus a neodesílají se data. Ale data která v prvku jsou odeslána ze serveru se zobrazí a nemažou. Proto bych myšlenku, že setDisabled() má aktuální hodnotu v prvku formuláře smazat úplně opustil. Jinak bude docházet k matoucím situacím, kdy někdy data v disabled prvku budou a jindy ne.

erased

Pokud by měl někdo potřebu mít metodu na spolehlivé smazání dat z formulářového prvku, můžeme přidat metodu setErased(), která data v prvku spolehlivě smaže, ale ne v závislosti na pořadí volání metod, ale vždy za každých okolností. Mazání dat bych ale z metody setDisabled() určitě navždy odebral.

Editoval m.brecher (7. 4. 0:53)

David Grudl
Nette Core | 8139
+
+2
-

Aha, já v tom stávající řešení readonly: četl setOmitted() místo setOmitted(false). Pak ten název setReadOnly dává smysl.

Taco
Člen | 46
+
0
-

Hele, vám to tak funguje?

$form[‚foo‘]->setDisabled()->setOmitted(true); // není vůbec ve výpise
$form[‚foo‘]->setDisabled()->setOmitted(false); // ve výpise je, ale není vyplněn; aby ne, když se vůbec nepošle

Aktuálně to řeším tím, že si posílám tu hodnotu ještě jednou v hidden, což pochopitelně není vůbec hezké.
Nebo si to nastavuju pomocí setDefaultValue(), ale to si zase vynucuje, abych tu entity vytahoval znova z databáze.

nette/forms v3.2.0

Editoval Taco (20. 4. 2:10)

m.brecher
Generous Backer | 758
+
0
-

@Taco

$form[‚foo‘]->setDisabled()->setOmitted(true); // není vůbec ve výpise

$form['foo']->setDisabled()->setOmitted(true)   // nedává smysl

Výše uvedený kód – kombinace ->setDisabled() + ->setOmitted(true) nedává smysl, protože ->setDisabled() vyřadí $value inputu z $values odeslaného formuláře. ->setOmitted(true) dělá totéž

$form['foo']->setDisabled()->setOmitted(false)   // funguje jako readonly

Výše uvedená kombinace funguje jako readonly nastavení na inputu, data vložená ->setDefaultValue() se zobrazí a nejdou editovat a jsou v odeslaných $values formuláře, ale za těchto dvou podmínek:

a) nejprve se na inputu zavolá ->setDisabled(), až potom se volá ->setDefaultValue()
b) ->setDefaultValue() se musí zavolat před událostí anchor formuláře

Pokud není splněno b) tak se může stát, že po submitu formuláře $value z disablovaného inputu zmizí.

Když je splněno a) i b), potom ->setDisabled()->setOmitted(false) i samotné ->setDisabled() funguje spolehlivě.

$form[‚foo‘]->setDisabled()->setOmitted(false); // ve výpise je, ale není vyplněn; aby ne, když se vůbec nepošle

Toto by mohl být případ, kdy není splněno pravidlo b)

@DavidGrudl v nedávném komentáři k disablování prvků uvedl, že default value lze nastavit u submitnutého formuláře „dokud neví že byl submitnutý“ tj. před událostí anchor https://forum.nette.org/…ba-framework#…

Takže to vypadá, že možnost udržet $value v disablovaném inputu a mít možnost readonly feature je možná nezamýšlený vedlejší efekt toho, že před anchorem formulář neví jestli byl submitnutý. Protože disabled prvek data skutečně neodešle a setOmitted(false) je do prvku dostane ze setDefaultValue(). Pokud by tam setDefaultValue() nebylo, nebo pokud setDefaultValue() zavoláme po anchoru, tak ani setOmitted(false) nepomůže.

Závěr:

Jakmile nějak pokročíme v setReadonly(), měli bychom přepracovat disablování prvků. Čím jsem si jistý je, že:

  • setDisabled() by v žádném případě neměl $value v inputu mazat, protože toto mazání je k ničemu a způsobuje část problému.
  • u disablovaného a readonly prvku by mělo být možné nastavit setDefaultValue() i v submitnutém formuláři a kdykoliv během životního cyklu formuláře, tedy např. i v události onRender[].

Já jsem dřív default values do formulářů plnil v onRender[] což si myslím je správné řešení, ale právě kvůli disablovaným prvkům jsem od toho upustil a krkolomě řeším jak naplnit formulář před anchorem, žádná událost před anchorem není. Dá se s tím žít, ale komplikuje to život. Mělo by se to vyřešit.

Mazání dat z inputů

Tato feature není moc často potřeba, ale jsou situace, kdy se hodí. Mazání dat ale musí být implementováno JINOU metodou než disablování. Potřebujeme mít obojí – 100% mazání a 100% disablování. Dnes to máme pohromadě v jedné metodě. Současně s přepracováním setDisabled() by se mohla přidat pro mazání speciální metoda např. setErased() aby bylo jasné, že vymaže data. Tato metoda by disablovala prvek jako setDisabled() a naíc by 100% vymazala $value z prvku.

Editoval m.brecher (20. 4. 15:21)

Taco
Člen | 46
+
0
-

m.brecher napsal(a):

$form['foo']->setDisabled()->setOmitted(false)   // funguje jako readonly

Výše uvedená kombinace funguje jako readonly nastavení na inputu, data vložená ->setDefaultValue() se zobrazí a nejdou editovat a jsou v odeslaných $values formuláře, ale za těchto dvou podmínek:

a) nejprve se na inputu zavolá ->setDisabled(), až potom se volá ->setDefaultValue()
b) ->setDefaultValue() se musí zavolat před událostí anchor formuláře

Pokud není splněno b) tak se může stát, že po submitu formuláře $value z disablovaného inputu zmizí.

Když je splněno a) i b), potom ->setDisabled()->setOmitted(false) i samotné ->setDisabled() funguje spolehlivě.

$form[‚foo‘]->setDisabled()->setOmitted(false); // ve výpise je, ale není vyplněn; aby ne, když se vůbec nepošle

Toto by mohl být případ, kdy není splněno pravidlo b)

Tak jsem si to právě vyzkoušel, a je to trochu jinak.
Ano, podmínka a) musí být splněna, což je IMHO jen komplikace a nemá to vliv na podstatu věci.
Podmínka b) neplatí. Nezáleží na tom.

Podstata věci je, se musí zavolat ->setDefaultValue() vždy (což je ostatně logické vzhledem k tomu, jak funguje html). Na tom jsem se spálil já, protože jsem si navykl plnit formulář jen jednou při inicializaci. No, tak v tom případě to s volbou setDisabled() dělat nemůžu.

Poznámka: disablování prvků by se mělo upravit tak, aby fungovalo spolehlivě 100% za všech okolností, aby to člověk použil a nemusel řešit, že různé věci okolo smí nebo nesmí. Jakmile nějak pokročíme v setReadonly(), tak bych se na tento problém podíval.

Se obávám, že bez nějakého hnusného hacku to nepůjde. Protože ten formulář tam tu hodnotu nemůže doplnit, když ji nemá = nepřišla v POST.

David Grudl
Nette Core | 8139
+
+2
-

To chování vychází z toho, že každý prvek má jednu aktuální hodnotu. Když volám setDefaults() nebo setDefaultValue() a nevím, jestli formulář byl nebo nebyl odeslán, hodnota se přijme. Protože pokud se pak zjistí, že odeslán byl, přepíše se hodnotou odeslanou uživatelem.

A do tohoto vstupuje disabled.

Pokud je prvek disabled a zjistí se, že formulář byl odeslán, odeslaná hodnota se ignoruje a zůstává původní výchozí hodnota.

Pokud se ale prvek stane disabled až ve chvíli, kdy již byla přijata hodnota zvenčí, není jiná cesta než je smazat.

m.brecher
Generous Backer | 758
+
0
-

@Taco

Podmínka b) neplatí. Nezáleží na tom.

Před 4 měsíci se o disablování vedla diskuse, kde se tento problém zmiňoval: https://forum.nette.org/…ba-framework#…

Zkusil jsem si teď ověřit moje tvrzení b) na nette/forms v3.2.1:

$form->addText('disabled', 'Disablováno')->setDisabled();
$form->onRender[] = fn() => $form['disabled']->setDefaultValue('abc');
$form->onValidate[] = fn() => $form->addError('error');  // vyvolám chybu, aby formulář zůstal v submitu

a potvrzuji, že máš pravdu a bod b) neplatí. Super, jeden problém je pryč.

Se obávám, že bez nějakého hnusného hacku to nepůjde. Protože ten formulář tam tu hodnotu nemůže doplnit, když ji nemá = nepřišla v POST.

Samozřejmě že funkce readonly jak ji navrhuji vyžaduje, aby se si vývojář nastavil setDefaults() a to jak pro vykreslený, tak i pro submitnutý formulář. Protože readonly prvek by měl být realizován jako html disabled s dodatečným doplněním default values po submitu.

m.brecher
Generous Backer | 758
+
0
-

@DavidGrudl

Pokud se ale prvek stane disabled až ve chvíli, kdy již byla přijata hodnota zvenčí, není jiná cesta než je smazat.

Ano, máš pravdu.

Jak se o disabled diskutovalo zde: https://forum.nette.org/…ba-framework#… tak dnes jsem provedl krátký test a problém s mizejícími daty v disabled prvku je spolehlivě vyřešen. To je super.

Pokud by se přidala metoda setReadonly(), tak by se do dokumentace mohlo zmínit, že pro správnou funkci setReadonly() je potřeba řádně nastavit setDefaultValue() i pro submitnutý formulář, protože reálně se data neodesílají ale berou se právě z default hodnoty.

Editoval m.brecher (20. 4. 16:48)

David Grudl
Nette Core | 8139
+
0
-

Nejsem si vůbec vědom, že bych tam něco v poslední době měnil…

m.brecher
Generous Backer | 758
+
0
-

@Taco

Nebo si to nastavuju pomocí setDefaultValue(), ale to si zase vynucuje, abych tu entity vytahoval znova z databáze.

pokud chceš pracovat s readonly prvky setDisabled()->setOmitted(false), tak musíš setDefaultValue() nastavovat jak u neodeslaného, tak i u odeslaného formuláře.

Já používám Nette Database Explorer a mám systém, kdy záznam se kterým formulář pracuje vždycky tahám z databáze a v presenteru ověřuji existenci záznamu (nemusí existovat) až potom vytvářím formulář.

tu entitu vytahoval znova z databáze

dospěl jsem k názoru, že záznam z databáze editovaný ve formuláři skoro vždycky potřebuji 3 x až 4 x použít a proto používám cachování v modelové třídě. Do modelové třídy nasetuji id záznamu a přes unifikovanou metodu getOne() tahám cachovaný ActiveRow záznam.

Zjednodušený kód:

class ArticleModel
{
    public readonly int $id;
    public readonly ?ActiveRow $row;

    public function setup(?int $id, ....): void   // setup v presenteru
    {
        $this->id = $id;
    }

    public function getOne(): ?ActiveRow
    {
        return $this->row ?? $this->row = $this->database->table('article')->get($this->id);
    }
}

Záznam použiji:

  • v presenteru – ověření existence záznamu
  • ve formuláři pro naplnění selectů
  • v setDefaults()
  • v handleru formuláře – handler volá metodu modelové třídy (updateOne(), deleteOne()) a metoda modelové třídy interně pracuje s cachovaným $row záznamem

Velkou výhodou je zjednodušení kódu, nemusím řešit předávání $id, jednoduše se $id jednou předá do modelové třídy a záznam $row je k dispozici tolikrát, kolikrát je potřeba. Formuláře tak jak je stavím já vůbec $id nepotřebují a pracují jenom s modelovou třídou, kterou si vyžádají pomocí DI.

Pokud pracuješ s Doctrine, tak tam nevím jak duplicitu sql řešit, protože s Doctrine nemám zkušenosti.

Taco
Člen | 46
+
+1
-

m.brecher napsal(a):

@Taco

Se obávám, že bez nějakého hnusného hacku to nepůjde. Protože ten formulář tam tu hodnotu nemůže doplnit, když ji nemá = nepřišla v POST.

Samozřejmě že funkce readonly jak ji navrhuji vyžaduje, aby se si vývojář nastavil setDefaults() a to jak pro vykreslený, tak i pro submitnutý formulář. Protože readonly prvek by měl být realizován jako html disabled s dodatečným doplněním default values po submitu.

Což mě osobně přijde pěkně na prd. Když vidím metodu setReadonly() tak očekávám, že to bude jen pro čtení. Podobně, když vidím, že je položka setDisabled(), že je disablovaná. V obou případech bych očekával, že hodnota přijde. V případě disabled možná chybně.

Vzhledem k tomu, že nejde rozumě a bez kouzel zajistit, aby tam ta hodnota, u disabled (a v důsledku ani u readonly), byla, tak bych na to rezignoval. A nechal bych to na setHtmlAttribute("disabled"). Protože to alespoň jasně přenechává zodpovědnost za chování na html.

m.brecher
Generous Backer | 758
+
0
-

@Taco

Což mě osobně přijde pěkně na prd. Když vidím metodu setReadonly() tak očekávám, že to bude jen pro čtení. Podobně, když vidím, že je položka setDisabled(), že je disablovaná. V obou případech bych očekával, že hodnota přijde. V případě disabled možná chybně.

Jak se od sebe setReadonly() a setDisabled() liší je potřeba si nastudovat. Html norma definuje atribut readonly, ten je ale špatně implementován. Html readonly prvky sice nejde editovat a odesílají value, ale mají focus, což je matoucí, a navíc nejdou nastavit na všech potřebných typech prvků.

To už je lepší aby framework měl vlastní implementaci readonly pomocí html atributu disabled, a počítat s tím, že do prvků musím dodat default values. Je ale určitě lepší používat setReadonly() než kombinaci setDisabled()->setOmitted(false). Líp to asi udělat nejde.

Editoval m.brecher (20. 4. 18:41)

Taco
Člen | 46
+
0
-

m.brecher napsal(a):
To už je lepší aby framework měl vlastní implementaci readonly pomocí html atributu disabled, a počítat s tím, že do prvků musím dodat default values.

Vzhledem k tomu, že jsem se na tom opakovaně spálil, tak mi to lepší nepřijde. Je to neintuitivní a nepřímočaré.

Představuji si, že když bych si funkcionalitu musel řešit sám pomocí setHtmlAttribute(), že by mě to trklo spíš. Dle hesla ať to FW dělá pořádně nebo vůbec.

David Grudl
Nette Core | 8139
+
0
-

Focus se dá vypnout před tabindex=-1 nebo tak nějak

m.brecher
Generous Backer | 758
+
0
-

@DavidGrudl

máš pravdu, že ohledně disabled prvků se v nette forms nic nezměnilo. Chyba tam ale skutečně je a konečně jsem vypátral, kde přesně. Disablovaný prvek po submitu formuláře při určité nepříznivé konstelaci skutečně data ztratí. Testoval jsem takovýto jednoduchý formulář:

 $form->addText('disabled', 'Disablovaný')
    ->setDisabled()->setOmitted(false);

$form->addSubmit('send', 'Odeslat')
    ->onClick[] = $form->handleSend(...);

$form['disabled']->setDefaultValue('abcd');         //  OK
$form->setDefaults(['disabled' => 'abcd']);         //    OK

$form->onRender[] = fn() => $form['disabled']->setDefaultValue('abcd');     // OK
$form->onRender[] = fn() => $form->setDefaults(['disabled' => 'abcd']);       //  NOT OK

$form->onValidate[] = fn() => $form->addError('is_submitted');

return $form;

V testovacím formuláři jsem zkoušel kombinovat nastavení default value před/po anchoru a metodou setDefaultValue() a setValues(). Vždycky jsem zkusil jeden řádek s nastavením default values a ve 3 případech to funguje OK a v jednom případě data po submitu zmizí. Problém má metoda setDefaults(), zatímco setDefaultValue() je OK.

Editoval m.brecher (20. 4. 21:20)

m.brecher
Generous Backer | 758
+
0
-

@DavidGrudl

Focus se dá vypnout před tabindex=-1 nebo tak nějak

bohužel html atributem readonly se nedají disablovat všechny formulářové prvky, kde to potřebujeme, třeba select. A zrovna select se občas hodí nastavit setReadonly().

Martk
Člen | 656
+
+1
-

OT: Jsem asi blbej, ale musím se zeptat. Proč zobrazovat datum vydání článku při vstoupení do formuláře, když datum je ještě neznámé? Když bude muset jet např. do nemocnice a pak vydá rozepsaný článek, tak najednou bude o 5 hodin starý a ne mezi novými, tam je rozhodující přece to kliknutí na odeslat. Pochopil bych ještě editovatelný čas a nemyslím to nějak zle, jen z přílišné zvědavosti se musím zeptat.

Pavel Kravčík
Člen | 1182
+
+1
-

Martk napsal(a):

OT: Jsem asi blbej, ale musím se zeptat. Proč zobrazovat datum vydání článku při vstoupení do formuláře, když datum je ještě neznámé? Když bude muset jet např. do nemocnice a pak vydá rozepsaný článek, tak najednou bude o 5 hodin starý a ne mezi novými, tam je rozhodující přece to kliknutí na odeslat. Pochopil bych ještě editovatelný čas a nemyslím to nějak zle, jen z přílišné zvědavosti se musím zeptat.

Protože ho chceš nastavit do minulosti například. Abys měl každý měsíc třeba 2 články, ale píšeš je až v dubnu… protože jsi líný prase a chtěl si je napsat v lednu. :)

Martk
Člen | 656
+
0
-

Abych to upřesnil, tak se jedná o disabled prvek při vytváření nového článku, takže běžný uživatel to nedokáže nastavit do minulosti aniž by mu běžel od ledna nonstop počítač s otevřeným tabem na formuláři. Proto jsem na konci věty psal, že bych pochopil, kdyby to byl editovatelný čas :)

Ptám se na tohle: Např. když vytvářím nový článek, potřebuji uložit do created_at aktuální datum, a potřebuji datum zobrazit + blokovat editaci.

V tomto případě se totiž „aktuální datum“ bude lišit od data vytvoření článku. Článek píšeš 30 minut, něco akutního ti do toho vleze, rozepsaný článek ze včerejška apod. a najednou to je včerejší článek a to asi nebude úplně cíl autora. Takže se mi to zdá spíše jako bug než feature. Datum vytvoření článku by se měl přiřadit teprve až v onSuccess a zobrazit jako disabled v editaci.

Lépe už to asi nedokážu popsat a nerad bych to tady zaspamoval, spíše mě zajímal odpověď autora.

Editoval Martk (22. 4. 13:09)

m.brecher
Generous Backer | 758
+
0
-

@Martk

Proč zobrazovat datum vydání článku při vstoupení do formuláře, když datum je ještě neznámé?

setReadonly() alias pro setDisabled()->setOmitted(false) není ve skutečnosti realizován html atributem readonly, ale disabled a data po submitu si znovu natáhne z nějaké metody, kterou si vývojář implementuje. Takže pokud použiješ třeba:

public function getDeaultArticleData(): array
{
    return [
        'craeted_at' => new DateTime();
        'state' => 'Draft',
    ];
}

Takže se ti neuloží přesně to datum, které je ve formuláři, ale datum skutečného uložení. Pokud uživatel píše článek o půlnoci, může se stát, že se mu uloží jiné datum, než vidí ve formuláři, to se ale bude lišit o jeden den.

Článek byl jenom příklad, readonly prvek můžeš použít, když budeš vystavovat fakturu, nebo nějaký dokument, který má algoritmem daný tvar, který vygeneruješ, chceš, aby ho uživatel viděl ale nemohl ho měnit. Tam všude readonly zjednodušuje život, nepoužije se moc často, ale jsou situace, kdy se hodí.

m.brecher
Generous Backer | 758
+
0
-

@Martk

Takže se mi to zdá spíše jako bug než feature.

Každá sebelepší feature se musí použít správným způsobem, jak už jsem napsal před chvílí, článek byl zjednodušený příklad.

m.brecher
Generous Backer | 758
+
0
-

@Taco

Představuji si, že když bych si funkcionalitu musel řešit sám pomocí setHtmlAttribute(), že by mě to trklo spíš. Dle hesla ať to FW dělá pořádně nebo vůbec.

Myslím, že Nette Framework podceňuješ. Pokud totiž použiješ html atribut readonly nebo disabled místo setDisabled()/setReadonly(), může Ti kdokoliv do „readonly“ nebo „disabled“ prvku podstrčit cokoliv a pak se může stát, že si uživatel přidělí vyšší roli než na kterou nemá právo apod…

Testoval jsem zda Nette zabezpečuje setDisabled() prvky proti hacknutí a výsledky potvrdily, že poskytuje maximální zabezpečení a to jak disablované inputy, tak i disablované submitButtons. Pokud útočník podstrčí do disabled/readonly nějakou svoji nebezpečnou hodnotu, Nette Framework to ignoruje.

Další použití setReadonly() bude u input hidden, protože i když je input hidden skrytý, hacknout jde úplně stejně jako viditelný.

V dokumentaci Nette k disablovaným prvkům bohužel není žádná zmínka o existujícím skvělém zabezpečení proti útoku, ani varování před html „readonly“ nebo „disabled“ prvků https://doc.nette.org/…rms/controls#…

Editoval m.brecher (22. 4. 15:40)

Martk
Člen | 656
+
0
-

Nepochopeno z důvodu, že tady neustále čtu, že se mají data odesílat na server, takže z toho vyplývalo, že je potřeba hodnota od uživatele i když je zrovna neaktuální/nevalidní. Ale ve skutečnosti jen chceš nastavenou hodnotu od programátora a ne odeslanou. Díky za upřesnění.

Existující metodu Nette formulářů setDisabled() sice lze použít pro zobrazení needitovatelné hodnoty, ale hodnota se na server neodesílá, což v řadě případů nevyhovuje.

  1. setReadonly() – data vidím, data nezmizí, nelze editovat, data se na server ODEŠLOU

Editoval Martk (22. 4. 15:19)

m.brecher
Generous Backer | 758
+
0
-

@Martk

Nepochopeno z důvodu, že tady neustále čtu, že se mají data odesílat na server

Ano, napsal jsem to příliš stručně takže to mohlo být nesprávně pochopeno. Proto si i myslím, že by se disabled/readonly prvky měly v dokumentaci víc vysvětlit – jak fungují, že vyžadují setDefaultData() a jak je to se zabezpečením proti útoku viz: https://forum.nette.org/…epsit-funkci#…

Taco
Člen | 46
+
0
-

m.brecher napsal(a):

@Taco

Představuji si, že když bych si funkcionalitu musel řešit sám pomocí setHtmlAttribute(), že by mě to trklo spíš. Dle hesla ať to FW dělá pořádně nebo vůbec.

Myslím, že Nette Framework podceňuješ. Pokud totiž použiješ html atribut readonly nebo disabled místo setDisabled()/setReadonly(), může Ti kdokoliv do “readonly” nebo “disabled” prvku podstrčit cokoliv a pak se může stát, že si uživatel přidělí vyšší roli než na kterou nemá právo apod…

Já Nette rozhodně nepodceňuji.

Opakováním pro zdůraznění: Mě sejří, že nemůžu vytvořit formulář, kterému nastavím hodnotu jednou při inicializaci. S volbou setDisabled() to přestane fungovat. Nejen, že mi to tu hodnotu nevrátí, ale při první chybě se to celé rozbije. Vím jak funguje html, a vadí mi to u něj úplně stejně.

Výsledek v mém případě je ten, že metodu setDisabled nepoužívám vůbec, a řeším to pomocí CSS, nebo jinak.

Argumentace podstrkáváním mě přijde zcela scestná. Protože všechna data posílaná a získaná od klienta jsou potencionálně nebezpečná bez ohledu na disabled. Třeba aktuální problém: chci editovat záznam, a chci disablovat typ záznamu, protože ten se měnit nemůže. I kdyby si to uživatel na své straně upravil, tak mi to chcípne na validaci, kterou samozřejmě dělám. Takže disabled tak jak existuje je pro mě jenom komplikace.

Šaman
Člen | 2635
+
0
-

Když chci uživateli nějakou hodnotu zobrazit, ale taky jasně ukázat že není editovatelná, taky používám CSS.

m.brecher
Generous Backer | 758
+
0
-

@Taco

Mě sejří, že nemůžu vytvořit formulář, kterému nastavím hodnotu jednou při inicializaci.

s tím je nejlépe se smířit, protože např. pro select nebo radio prvky musíš také dodat data vždycky – při inicializaci i po submitu. Já formuláře dědím z abstraktního předka a ten naplní formulář daty automaticky a ručně řešit to nemusím. Když nastavím prvku setDisabled(), nemusím už dělat nic dalšího, protože data se tam plní automaticky vždycky. Je to celé rozsáhlé a OT tohoto vlákna tak jenom to hlavní:

V modelové třídě mám VŽDY tyto metody kterými plním data do formuláře:

public function getDefaultOne(): array
{
	return [
        'state' => 'Draft',
        'created_at' => new DateTime()
    ];
}

public function getOne(): ?ActiveRow
{
    return $this->database->table('article')->get($this->id);
}

Formulář dědí z abstraktního předka FormControl, který dědí z UI\Control. FormControl má v sobě mimo jiné tento kód:

abstract protected function build(): BaseForm;

public function startup(): void
{
    if(isset($this->model) && $this->model->mode === Mode::Update && !$this->model->getOne()){
        $this->error('Záznam nebyl nalezen');
    }
    // ......
    $form = $this->build();
    $this->addComponent($form, 'form');
   // .......
    $form->onRender[] = $this->setDefaultData(...);  / zde se automaticky a vždycky naplní data
}

private function setDefaultData(BaseForm $form): void  // vybere z modelu správnou metodu podle módu formuláře
{
    match($this->model->mode){
        Mode::Create => $form->setDefaults($this->model->getDefaultOne()),
        Mode::Update => $form->setDefaults($this->model->getOne()),
        default => throw .....,
    };
}

Finální třídu formuláře podědíš z FormControl, implementuješ metodu build() která vytvoří formulář. Abstraktní předek se automaticky postará o naplnění formuláře daty a nemusíš psát hromadu nudného opakujícího se kódu. Potomek FormControl dělá kompletní servis formuláři, který má kompozicí vnořen ve svojí metodě. Obstará úkony, které v Nette aplikacích běžně dělá presenter nebo factory. Presentery, modelové třídy i finální formuláře mám jednoduché, protože ten kdo nejvíc pracuje a maká je abstraktní předek FormControl. Presenter v akci přidá finální třídu formuláře a spustí na ní startup().

Napsal jsem to sem proto, že si myslím, že problém nudného opakujícího se kódu vyřeší jedině nějaký systém a chytře vymyšlené abstraktní třídy, které by převzaly opakující se činnosti na svoje bedra.

Editoval m.brecher (23. 4. 1:55)

Taco
Člen | 46
+
0
-

m.brecher napsal(a):

@Taco

Mě sejří, že nemůžu vytvořit formulář, kterému nastavím hodnotu jednou při inicializaci.

s tím je nejlépe se smířit, protože např. pro select nebo radio prvky musíš také dodat data vždycky – při inicializaci i po submitu.

Select či Radio options nejsou value hodnoty. Tudíž je to něco jiného. Tudíž se s tím nesmiřuju :-)

Abys dobře pochopil sílu mého odporu, důsledně všude nepoužívám setDefaultValue() nikdy a vůbec. Formulář plním na jednom místě. Ze zásady :-)

Editoval Taco (23. 4. 2:15)

Kamil Valenta
Člen | 762
+
0
-

Pavel Kravčík napsal(a):

Martk napsal(a):

OT: Jsem asi blbej, ale musím se zeptat. Proč zobrazovat datum vydání článku při vstoupení do formuláře, když datum je ještě neznámé? Když bude muset jet např. do nemocnice a pak vydá rozepsaný článek, tak najednou bude o 5 hodin starý a ne mezi novými, tam je rozhodující přece to kliknutí na odeslat. Pochopil bych ještě editovatelný čas a nemyslím to nějak zle, jen z přílišné zvědavosti se musím zeptat.

Protože ho chceš nastavit do minulosti například. Abys měl každý měsíc třeba 2 články, ale píšeš je až v dubnu… protože jsi líný prase a chtěl si je napsat v lednu. :)

Pokud chci umožnit publikovat v minulosti, pak přece nebudu řešit readonly / disabled.

Naopak mně připadá nežádoucí, aby se disabled / readonly prvky na server odesílaly. Proč, když na ně uživatel nesmí sahat? Pokud si backend ta data neobstará jinak a sáhne pro ně do values, slepě důvěřuje, že uživatel ta data skutečně nezměnil. Jenže toho readonly / disabled zárukou není.

m.brecher
Generous Backer | 758
+
0
-

@KamilValenta

Pokud si backend ta data neobstará jinak a sáhne pro ně do values, slepě důvěřuje, že uživatel ta data skutečně nezměnil. Jenže toho readonly / disabled zárukou není.

Pokud použiješ html atributy disabled nebo readonly tak ano, jde to podstrčit/hacknout. Ale když použiješ metody Nette setDisabled() nebo setReadonly() (alias pro setDisabled()->setOmitted(false)) tak Nette proti podstrčení 100% chrání, jenom to není napsané v dokumentaci. Když se použije setDisabled()->setOmitted(false) tak se data jakoby „odesílají“, ale ve skutečnosti se neodesílají, ale znovu se vytáhnou ze setDefaultValue().

Položím otázku – věděl Jsi, že metoda setDisabled() poskytuje 100% ochranu proti podstrčení dat ??

Kamil Valenta
Člen | 762
+
0
-

m.brecher napsal(a):

tak se data jakoby „odesílají“

Pokud řešíme „jakoby odesílají“ vs. „odesílají“, tak je to v pořádku.
Já myslel, že vlákno je o odesílaných datech na server, protože v úvodním postu jsi napsal:
„Ideální tedy je použít pro readonly prvky formulářů setDisabled(), který by ale odesílal hodnotu na server.“
A taková metoda imho potřeba není právě pro její nebezpečnost.

Položím otázku – věděl Jsi, že metoda setDisabled() poskytuje 100% ochranu proti podstrčení dat ??

Ano.

m.brecher
Generous Backer | 758
+
0
-

Ideální tedy je použít pro readonly prvky formulářů setDisabled(), který by ale odesílal hodnotu na server.

To jsem napsal blbě, myslel jsme tím „kdy by hodnota byla ve výsledku“ takže to zmátlo více lidí.

Ano.

Já ne, protože v dokumentaci to napsané není a tak jsem předpokládal, že případně podstrčená data si musím ošetřit sám. Takže setReadonly() postavené na setDisabled() je bezpečné na rozdíl od html readonly tak je to o důvod navíc proč tato nová metoda byla užitečná. Ale do dokumentace bych přidal podrobnější informace a) že setReadonly() je bezpečné, b) pro svoji funkci vyžaduje nastavení setDefaultValue() c) interně nepoužívá html readonly ale disabled.

Davidovi přijde název setReadonly() matoucí, protože by vývojáři mohli předpokládat, že použije html readonly a navrhuje jiný název, třeba preserveDefaultValue(), nebo setPreserve(). Mě se ale zdá, že jediný srozumitelný název je setReadonly(), protože funkce je „bezpečné readonly“. Nebo teď mě napadlo setSafeReadonly(), když se Davidovi setReadonly() nelíbí.

jeremy
Člen | 50
+
0
-

Já ne, protože v dokumentaci to napsané není a tak jsem předpokládal, že případně podstrčená data si musím ošetřit sám.

Deaktivace prvku – Mně z dokumentace přijde jasný, že setDisabled() automaticky ochrání před podvržením dat. Je tam vysvětlený téměr všechno o čem tu mluvíš. Vzhledem k tomu jak málo by se něco takovýho stejně dalo využít, proč prostě v takovém případě nenapsat o pár znaků navíc.
setReadOnly() je matoucí, vzhledem k tomu jak readonly v html normálně funguje.

Ještě k tomu příkladu jak jsi uváděl před tím. Datum, které se ukáže uživateli ve formuláři je pouze informace kdy právě uživatel začal vytvářet příspěvek. Přijde mi jako špatný desgin takovou informaci pojmenovat created at, protože to nevystihuje co ta informace říká.
Lepší by to bylo pojmenovat například draft started at a jak se formulář odešle, tak do databáze created_at uložit opravdový datum vytvoření příspěvku.

Popravdě bych řekl, že readonly má málo kdy využití v dobře navrženym formuláři.

Ps: Myslim, že to v dokumentaci bylo už před tím, ale nejsem si na 100% jistý. Každopádně bylo to tam ještě než David napsal, že neví proč by měl něco do dokumentace přidávat, protože mu to přišlo jasné.

Editoval jeremy (23. 4. 14:27)

m.brecher
Generous Backer | 758
+
0
-

@jeremy

Mně z dokumentace přijde jasný, že setDisabled() automaticky ochrání před podvržením dat.

Máš pravdu, bezpečnostní aspekty disabled a readonly jsou nyní v dokumentaci velmi dobře popsány. Už jen zbývá vyřešit název metody, která by zapouzdřila nyní používanou dvojici setDisabled()->setOmitted(false). Najít název, ze kterého by bylo jasné, že se jedná o bezpečnou variantu k nezabezpečenému html readonly.

jeremy
Člen | 50
+
+1
-

Já bych to nechal jako setDisabled() protože to pořád je disabled prvek. Pokud na zkratce pro setDisabled()->setOmitted(false) tak trváš, tak mi jako nejlepší nápad přijde přidat parameter: bool $preserveDefaultsetDisabled(bool $value = true, bool $preserveDefault = false).
Pak by použití bylo: setDisabled(preserveDefault: true). (Popřípadě to zkrátit jen na bool $preserve)

jeremy
Člen | 50
+
0
-

Jeste mozna lepsi nez bool $preserve by bylo bool $persistent aby se schodovalo s funkcnosti hidden prvku.

Kamil Valenta
Člen | 762
+
+5
-

To skutečně každá dvojice metod, ze které je na první pohled jasný význam, potřebuje wrapper metodu, kde k názvu vs. významu potřebujeme 2 vlákna fóra a upravenou dokumentaci?

Pokud je navíc ojedinělá využitelnost pro pár lidí, proč si takový wrapper nedáte do společného předka pro Form?

m.brecher
Generous Backer | 758
+
0
-

@KamilValenta

Pokud je navíc ojedinělá využitelnost pro pár lidí, proč si takový wrapper nedáte do společného předka pro Form?

Metoda setReadonly() by se měla přidat do BaseControl, což je abstraktní třída, ze které dědí formulářové prvky. V PHP je možné do abstraktní třídy přidat metodu, která by byla přítomná v odvozených třídách jedině přímým zápisem do kódu třídy.