Pamiętasz przygody Brudnego Harry'ego związane z utratą danych podczas przeładowywania... nie, nie broni, formularza? Okazało się, że od ostatniego wpisu Harry się wycwanił i kontroluje formularz łypiąc okiem ze swojego hamaka. Przymyka oko wtedy, kiedy dane w formularzu nie uległy zmianie. Zrywa się wtedy, kiedy ktoś w nich grzebał i chce uciec z formularza bez zapisu.
W poprzednim odcinku.
Wykorzystywaliśmy wtedy zdarzenie beforeunload, ale zawsze kiedy zaszła jakaś zmiana w formularzu. Nie zwracaliśmy uwagi czy ktoś skasował w jakimś polu literkę i za chwilę ja dodał ponownie, czy też odznaczył jakiegoś ptaszka, aby zaraz znów go zaznaczyć. Czyli nawet w sytuacji interakcji z formularzem, ale braku realnych zmian, blokowaliśmy wyjście z formularza. W zależności od typów pól podpinaliśmy odpowiednie zdarzenia, a w nich dodawaliśmy atrybut data-isdirty z wartością true. Podczas próby wyjścia ze strony sprawdzane było istnienie i wartość atrybutu. Jeśli egzystował, to niweczyliśmy próbę wyjścia ze strony. Jego brak mógł być spowodowany albo brakiem interakcji, albo wciśnięciem przycisku zatwierdzania (zdarzenie submit jest wywoływane wcześniej niż beforeunload, tak więc w jego środku można swobodnie usunąć wspomniany atrybut). Tak było, jak bum cyk cyk!
W dzisiejszym odcinku.
Połowa naszego skryptu nie będzie się różnić od tego wcześniejszego - znów podepniemy odpowiednie zdarzenia, a w nich będziemy dodawać atrybut data-isdirty do formularza. Ale tylko kiedy zajdzie taka potrzeba. Ponieważ potrzeba matką wynalazków, stwórzmy drugą połowę skryptu, który będzie kontrolował wartości pól formularza.
Domyślne wartości pól (te po otwarciu formularza) będziemy przechowywać w tablicy. Ponieważ nie każde pole musi mieć nadane unikalne id, musimy je jakoś rozróżnić, aby móc wrzucać ich wartości pod określonymi kluczami w tej tablicy.
const initialValues = [];
/* tu pętla po polach,
a w środku tworzymy
unikalny identyfikator:
*/
const uid = i + "-" + Date.now();
/* który przypisujemy
do pola
*/
el.dataset.uid = uid;
Jest jedno, małe utrudnienie. W przypadku pól typu radio nie możemy nadawać dla każdego z przycisków osobnego uid, bo całość stanowi jedno pole. Tak więc natrafiając na radio musimy sprawdzić czy ma nadane uid, jeśli nie, to dodajemy je do wszystkich radio o tej samej nazwie. W przypadku tego pola, checkbox oraz SELECT zapisujemy w tablicy wybraną / klikniętą wartość.
Ta część skryptu wygląda tak:
const form = document.querySelector("form[data-isdirty]");
const initialValues = [];
form.querySelectorAll("input, select, textarea").forEach(function(el, i) {
/* czy element ma nadane uid?
*/
if (!el.dataset.uid) {
const uid = i + "-" + Date.now();
/* jeśli to radio
*/
if (el.type === "radio") {
/* to nadajemy wszystkim
przyciskom w tym formularzu
to samo uid
*/
form
.querySelectorAll('input[type="radio"][name="' + el.name + '"]')
.forEach(function(el) {
el.dataset.uid = uid;
/* i zapisujemy wartość
klikniętego (jeśli taki jest)
*/
if (el.checked) {
initialValues[uid] = el.value;
}
});
} else {
/* w przypadku pozostałych
pól jest prościej
*/
el.dataset.uid = uid;
/* kliknięty checkbox
*/
if (el.type === "checkbox") {
if (el.checked) {
initialValues[uid] = el.value;
}
/* wybrana opcja <select>
*/
} else if (el.type === "select") {
initialValues[uid] = el.querySelector("option:checked").value;
} else {
/* pole tekstowe lub textarea
*/
initialValues[uid] = el.value;
}
}
}
});
Posiadając nasz wynalazek możemy go wykorzystać dla drugiej połowy skryptu, czyli tego z poprzedniego wpisu, ale delikatnie zmodyfikowanego:
/* podłączamy zdarzenie input
(czyli wprowadzania danych,
również przez wklejanie)
*/
form.addEventListener("input", function(e) {
/* dla pól tekstowych i textarea
*/
if (
(e.target.tagName === "INPUT" &&
e.target.type !== "radio" &&
e.target.type !== "checkbox") ||
e.target.tagName === "TEXTAREA"
) {
/* ustawiamy wartość atrybutu
w zależności czy aktualna
wartość pola jest taka sama
czy też różni się od domyślnej
*/
this.setAttribute(
"data-isdirty",
initialValues[e.target.dataset.uid] !== e.target.value
);
}
});
Dla pól radio, checkbox i SELECT podepniemy się pod zdarzenie change. Dlaczego? Ponieważ niektóre przeglądarki (Edge czy Safari) nie obsługują zdarzenia input na tych polach. Tu będzie też trochę trudniej:
form.addEventListener("change", function(e) {
if (
(e.target.tagName === "INPUT" &&
(e.target.type === "radio" || e.target.type === "checkbox")) ||
e.target.tagName === "SELECT"
) {
/* jeśli to checkbox
i został odkliknięty,
to porównujemy z wartością
undefined, bo nie możemy
z wartością checkboxa,
ponieważ ten pomimo odkliknięcia
ma nadal ustaloną wartość
(w atrybucie value)
*/
if (e.target.type === "checkbox" && !e.target.checked) {
this.setAttribute(
"data-isdirty",
initialValues[e.target.dataset.uid] !== undefined
);
/* a dla pozostałych pól
*/
} else {
this.setAttribute(
"data-isdirty",
initialValues[e.target.dataset.uid] !== e.target.value
);
}
}
});
Teraz Brudny Harry wraca na hamak. Do przeczytania!
Przydatne linki:
Skrypt zabezpieczenia formularza w przypadku zmiany danych z tego wpisu.
Brudny Harry, czyli ochrona formularza przed przeładowaniem i utratą danych.
Zdarzenia change oraz input w JavaScript.