RFC – protection duplicate form submit – přidat custom event ‚formSuccess‘ do metody Nette.initForm()

m.brecher
Generous Backer | 863
+
+1
-

Ahoj !

Každý časem narazí na problém, že když zákazník při rezervaci/objednávce omylem dvakrát klikne na „Submit“ tlačítko, odešle se duplicitní request, který komplikuje život.

Na fóru jsem našel tuto starší diskuzi:

https://forum.nette.org/…ni-formulare

a tuto obsáhlou diskuzi s různými řešeními:

https://forum.nette.org/…itko-odeslat

V principu můžeme použít dvě cesty:

  • javascript – zablokovat odesílací tlačítko po prvním submitu validního formuláře
  • server – pomocí unikátního id nebo tokenu apod. znemožnit duplicitní uložení jednoho formuláře

Obě metody lze kombinovat.

Myslím, že nějaké solidní řešení v javascriptu, které není všemocné, ale zablokuje 90% všech případných duplicit, je vhodné podpořit v javascriptu netteForms.js.

Ideální fází zpracování odeslání formuláře, kde je vhodné blokovat duplicitní kliknutí „Submit“ tlačítka je událost javascriptu ‚submit‘ pokud současně netteForms.js vyhodnotí formulář jako validní a neblokují odeslání pomocí event.preventDefault() (obdoba nette/forms události onSuccess). Zde se přímo nabízí implementovat v netteForms.js javascriptový custom event ‚formSuccess‘, na který lze jednoduše zavěsit vlastní listener a spustit ověření duplicity.

Padl i návrh od @DavidGrudl zabudovat nějakou formu ochrany proti duplicitnímu submitu přímo do netteForms.js:

https://forum.nette.org/…itko-odeslat#…

Návrh

Navrhuji přidat do kódu netteForms.js v metodě Nette.initForm(form) custom event ‚formSuccess‘

Nette.initForm = function(form) {
    if(form.method === 'get' && form.hasAttribute('data-nette-compact')) {
        form.addEventListener('formdata', (e) => this.compactCheckboxes(form, e.formData));
    }

    if (!Array.from(form.elements).some((elem) => elem.getAttribute('data-nette-rules'))) {
        return;
    }

    this.toggleForm(form);

    if (form.noValidate) {
        return;
    }
    form.noValidate = true;

    form.addEventListener('submit', (e) => {
        if (!this.validateForm(form)){
            e.stopPropagation();
            e.preventDefault();

        }else{     // přidaný custom event 'formSuccess'
            window.dispatchEvent(new CustomEvent('formSuccess', {detail: {event: e, form: form}}))
        }
    });

    form.addEventListener('reset', () => {
        setTimeout(() => this.toggleForm(form));
    });
}

Potom si každý může jednoduše na event ‚formSuccess‘ zavěsit ochranu proti duplicitnímu submitu:

window.addEventListener('formSuccess', event => formUtils.checkDuplicateSubmit(event.detail.event, event.detail.form))

Já pro ochranu proti duplicitnímu submitu používám např. tento jednoduchý javascript:

class FormUtils
{
    #protectionDelayMs = 1000

    checkDuplicateSubmit(event, form)
    {
        if(!form.dataset.dateTimeSubmitted){
            form.dataset.dateTimeSubmitted = Date.now().toString()

       }else if(Date.now().toString() - form.dataset.dateTimeSubmitted < this.#protectionDelayMs){
            event.preventDefault()
        }
    }
}
const formUtils = new FormUtils()

Tento javascript po dobu 1000 ms po odkliknutí validního submitu blokuje další submit. Není to 100% ochrana, ale na nenáročné aplikace postačí. Každý si může na navrhovaný event navěsit takový javascript, jaký mu v dané situaci vyhovuje.

Jiné možnosti

Sice je možné místo custom eventu ‚formSuccess‘ si udělat vlastní listener na události ‚submit‘ a přes veřejné api Nette.validateForm(form) formulář validovat, to ale může být problematické. Validace se pak provádí dvakrát a pokud současně pomocí javascriptu zobrazujete chyby formuláře nějakým custom způsobem, působí to problémy.

Navržený custom event je elegantní, s minimální změnou v kódu, bez BC breaku. Umožňuje každému flexibilně použít ochranný javascript dle situace. Najít nějaké univerzální řešení pro každého a implementovat ho do netteForms.js asi ani nepůjde, vzhledem k tomu, že konkrétní formuláře se mohou hodně lišit.

PR

Prosím o komentáře a po diskusi bych podal PR, aby to tady nezapadlo ;)

David Grudl
Nette Core | 8218
+
+3
-

Tohle by byla užitečná featura, ale je potřeba jí udělat perfektně. IMHO nejsnazší cesta je na chvíli zablokovat tlačítko pomocí disabled = true. Jen na několik vteřin. Zároveň by se mělo ihned odblokovat, když uživatel změní nějakou hodnotu ve formuláři, když klikne na jiné tlačítko, když klikne na odkaz, když se nezdaří načtení stránky (událost window.onerror), když stiskne escape.

Možná by se to dalo zjednodušit i tak, že se tlačítko odblokuje, když uživatel kamkoliv (krom toho tlačítka) klikne nebo cokoliv stiskne.

Tlačítko by se asi naopak nemělo blokovat, když odešlu formulář a držím přitom nějakou klávesu shift, alt, ctrl, protože v tu chvíli provádím speciální akci. Možná by se nemělo blokovat u GET formulářů, podobně jako se neblokují klasické odkazy.

m.brecher
Generous Backer | 863
+
0
-

@DavidGrudl

je potřeba jí udělat perfektně

To by bylo ideální mít ochranu zabudovanou v netteForms.min.js, využil by to asi každý.

Moje poznámky k navrženým situacím, kdy blokování tlačítka ihned ukončit:

a) když uživatel změní nějakou hodnotu ve formuláři = > OK

b) klikne na jiné tlačítko = > OK

c) klikne na odkaz = > asi je zbytečné, ale ničemu to nevadí

d) při události window.onerror = > OK – velmi důležité

e) když stiskne Escape = > asi je zbytečné, ale ničemu to nevadí

Poznámky k situacím, kdy by se tlačítko nemělo blokovat vůbec:

f) když odešlu formulář a držím přitom nějakou klávesu shift, alt, ctrl, protože v tu chvíli provádím speciální akci = > OK

g) u GET formulářů = > OK

V zásadě mě navržená omezení přijdou velmi dobře promyšlená a pokud by takto byla ochrana v netteForms.js provedena, potom asi bude zbytečné, aby si Nette vývojáři psali vlastní méně dokonalá řešení a RFC navrhovaný event ‚formSuccess‘ by nebyl vůbec potřeba.

Editoval m.brecher (7. 10. 17:55)