Formularze

Za pomocą Javascriptu możemy w łatwy sposób kontrolować zachowanie się formularzy znajdujących się na stronie.
Częstokroć do kontroli danych pochodzących z formularzy stosuje się skrypty działające po stronie serwera (np w technologii PHP, CGI).

Czasami chcemy mieć możliwość natychmiastowej reakcji na poczynania użytkownika (np. wyświetlenie informacji gdy użytkownik wprowadzi błędne imię). Wysłane dane można oczywiście dalej sprawdzić za pomocą wyżej wymienionych języków, co da nam o wiele większe szanse na to, że dane te są prawidłowo wprowadzone.

Przykładowy formularz

Naukę działania na elementach formularzy przeprowadzimy na przykładowym formularzu:


<form id="myForm" method="post" action="skrypt_przetwarzajacy.php">
    <div>
        <label for="name">Imię:</label>
        <input type="text" name="name" id="name" value="" />
    </div>

    <div>
        <label for="email">E-mail:</label>
        <input type="email" name="email" id="email" value="" />
    </div>

    <fieldset>
        <legend>Wybierz płeć</legend>
        <input type="radio" value="m" name="gender" checked="checked" /> m
        <input type="radio" value="k" name="gender" /> k
    </fieldset>

    <fieldset>
        <legend>Wybierz dwa (2) ulubione owoce</legend>
        <input type="checkbox" value="jagoda" name="color" /> niebieski
        <input type="checkbox" value="jablko" name="color" /> czerwony
        <input type="checkbox" value="arbuz" name="color" /> zielony
        <input type="checkbox" value="pomarancz" name="color" /> pomarańcz
    </fieldset>

    <div>
        <button type="submit">Zapisz</button>
    </dov>
</form>

Pola typu TEXT

Zacznijmy od wypisania w konsoli właściwości pobranego elementu:


    <input type="text" name="inputText" id="inputText" placeholder="Pole testowe">

console.log('Pole tekstowe:');
console.dir(document.querySelector('#inputText'));

Jak widzisz w konsoli debugera, pole takie ma multum właściwości. Tak samo jak w przypadku innych elementów pobranych ze strony, większość tych właściwości przydaje się w specyficznych sytuacjach.

Najczęściej używaną właściwością będzie value, która zawiera ni mniej, ni więcej tylko wartość danego pola. Jest to atrybut danego pola, więc pomyślisz, że powinieneś użyć getAttribute('value'). Byłby to jednak błąd, gdyż getAttribute zwraca wartość atrybutu, jaki jest w html. Wartość value zwraca dynamiczną wartość, jaka jest wpisywana do danego pola.


//przez id
document.querySelector('#imie').value

//lub przez name
document.querySelector('#myForm input[name="imie"]').value

Pamiętaj, że właściwość name jest unikalna tylko w obrębie danego formularza. Jeżeli będzie więcej takich formularzy na stronie, wtedy istnieje możliwość, że taka nazwa może się powtórzyć. Dlatego właśnie w drugim przypadku pobieramy pole przez id formularza.

Pobrana wartość jest stringiem, możemy więc wykonać na niej wszystkie operacje, które możemy wykonywać na tekstach. I tak na przykład:


const val = document.querySelector("#name").value;

if (val.length == 0) {
    console.log('Nie wpisałeś żadnej wartości!')
} else {
    console.log('Twój imię to: ' + val.charAt(0).toUpperCase() + val.slice(1) );
}

Kolejny przykład pokazuje, jak możemy sprawdzić czy wpisywany ciąg ma odpowiednią formę. W przykładzie za pomocą wyrażeń regularnych sprawdzamy, czy użytkownik wpisał prawidłowe Imię:


<form method="post" action="...">
    <label for="userName">Podaj imię (min. 3 znaki nei będące cyframi):</label>
    <input type="text" id="userName" name="userName" value="" />
</form>

document.querySelector('#userName').on('change', function() {
    const val = this.value;
    const reg = /^[a-zA-ZąĄćĆęĘłŁńŃóÓśŚźÓżŻ]{3,}$/g; //testujące wyrażenie regularne
    if (!reg.test(val)) {
        alert("Co to za dziwne imię?...");
        this.select(); //zaznaczamy treść pola
    } else {
        alert('Twój imię to: ' + val.charAt(0).toUpperCase() + val.slice(1) );
    }
});

Zastosowanie okienek alert jest bardzo proste w implementacji, jednak nie jest najbardziej przyjemną metodą informowania o błędnie wprowadzanych danych. Informację o złych danych lepiej wyświetlać w nieco bardziej subtelny sposób. Można np wyświetlać przy błędnie wypełnionym polu komunikat lub zaznaczać pole za pomocą dodatkowej klasy:


<input type="text" name="name" id="name" value="" />
<span class="error">źle</span>

    document.getElementById('name').addEventListener('change', function() {
        const className = 'error-field';
        const reg = new RegExp('^[a-zA-Z]{3,}$', 'g');

        if (!reg.test(this.value)) {
            this.classList.add(className); //dodaję klase do pola
        } else {
            this.classList.remove(className); //usuwam klasę
        }
    });

Pokazanie komunikatu z informacją w powyższym przypadku możemy zrobić za pomocą JS


if (!reg.test(this.value)) {
    this.classList.add(className);
    this.nextElementSibling.style.display = "block";
} else {
    this.classList.remove(className);
    this.nextElementSibling.style.display = "none";
}

Lub za pomocą selektora CSS:


/* pole normalne */
.form .field {
    ...
}

/* pole z błędem */
.form .field-error {
    ...
}

/* text błędu */
.form .error-text {
    display:none;
}

/* text błędu za polem z błędem */
.form .field.field-error + .error-text {
    display:block;
}

Logiczne wydaje się, by sprawdzać dane po wywołaniu zdarzenia change. W końcu chcemy sprawdzić czy nowa (czyli zmieniona) wartość jest właściwa. Jest jednak pewne małe ALE. Przeglądarki nie odpalają tego zdarzenia, jeżeli wprowadzaną wartość wybierzemy z auto uzupełnienia (czyli pola, które pojawia nam się przy wypełnianym polu i daje możliwość wyboru wcześniej wprowadzonych odpowiedzi). Dlatego też w chwili obecnej nie możemy do końca polegać na zdarzeniu onChange. Są z tego dwa wyjścia. Jedno - stosowanie zdarzenia blur (czyli - po odznaczeniu/opuszczeniu danego elementu - w tym przypadku musimy przerywać sprawdzanie, gdy w pole zostanie wprowadzony pusty ciąg znaków (czyli gdy użytkownik tylko przeszedł przez pole).), lub ustawienie danemu polu atrybutu autocomplete="false". Bardziej logicznym rozwiązaniem wydaje się to drugie, jednak wiąże się z nim wprowadzenie utrudnienia dla użytkownika.

Pola typu Email

Kolejne pole w naszym formularzu powinno zawierać prawidłowy e-mail. Aby sprawdzić tą wartość, wystarczy zastosować wyrażenie regularne, które opisuje wygląd prawidłowego maila:


document.querySelector('email').addEventListener('change', function() {
    const className = 'errorField';
    const mailReg = new RegExp('^[0-9a-z_.-]+@[0-9a-z.-]+\.[a-z]{2,3}$', 'i');

    if (!mailReg.test(this.value)) {
        this.classList.add(className); //dodaję klase do pola
    } else {
        this.classList.remove(className);
    }
});

Powyższe wyrażenie regularne to przykład. Takich wzorów jest masa i na ich temat toczy się nie jedna dyskusja.

W poniższym formularzu sprawdzam tylko email:

Podobne techniki możemy stosować do większości typów danych - np do sprawdzania kodów pocztowych, miejscowości itp. Wystarczy odpowiednio opisywać takie dane za pomocą wyrażeń regularnych. Z drugiej strony nie powinniśmy też przesadzać z dokładnością sprawdzania takich danych. W końcu nie zawsze przeciętny użytkownik chce podawać dokładnie swoje dane. Czasami po prostu chcę sobie stworzyć konto "na szybko".

Pola typu RADIO

Zaczynamy od wypisania danych pól w konsoli:

tak nie

<p id="radioTestCnt">
    <input type="radio" value="tak" name="radioTest" /> tak
    <input type="radio" value="nie" name="radioTest" /> nie
</p>

console.log('Pola radio:');
console.dir(document.querySelectorAll('#radioTestCnt input[name="radioTest"]'));

Przyciski radiowe umożliwiają wybór tylko jednej opcji z pośród kilku. Tak więc powinniśmy je stosować w przypadkach "albo". Albo lubię czekoladę, albo nie lubię ;).
Każde pole należące do jednej grupy powinno mieć wspólną nazwę, ale indywidualną wartość (no chyba, że w szczególnych przypadkach...).


<fieldset>
    <legend>Wybierz płeć</legend>
    <input type="radio" value="m" name="gender" /> m
    <input type="radio" value="k" name="gender" /> k
</fieldset>

function checkRadio() {
    const genderRadio = this.form.querySelectorAll('input[name="gender"]');
    for (let i=0; i<genderRadio.length; i++) {
        if (genderRadio[i].checked) {
            alert('Zaznaczyłeś opcję nr: ' + i);
            break;
        }
    }
}

document.addEventListener("DOMContentLoaded", function() {
    const form = document.querySelector('#myForm');
    const genderRadio = form.querySelectorAll(':scope input[name="gender"]');

    for (let i=0; i<genderRadio.length; i++) {
        genderRadio[i].addEventListener('click', checkRadio);
    }
});
Wybierz płeć m k

Jak sprawdzić czy cokolwiek zostało wybrane (powiedzmy, że nie ustawimy właściwości checked dla żadnego pola)?
Wystarczy na samym początku funkcji ustawić dodatkową zmienną na -1. Następnie wykonując pętlę sprawdzamy, czy któreś pole jest zaznaczone. Jeżeli tak, naszej zmiennej przypisujemy numer zaznaczonego pola (czyli wartość od 0 do guziki_radio.length). Na przykład:


let checkedIndex = -1;
const gender = document.querySelectorAll('#myForm input[name="gender"]);

for (let i=0; i<gender.length; i++) {
    if (gender[i].checked) {
        checkedIndex = i;
        break;
    }
}

if (checkedIndex === -1) {
    alert('Nie zaznaczyłeś żadnego pola!');
}

Pola typu CHECKBOX

Zacznijmy od wypisania przykładowych checkboxów w konsoli:

niebieski czerwony

<p id="checkboxTestCnt">
    <input type="checkbox" value="niebieski" name="chkTest" /> niebieski
    <input type="checkbox" value="czerwony" name="chkTest" /> czerwony
</p>

console.log('Pola checkbox:');
console.dir(document.querySelectorAll('#checkboxTestCnt input[name="chkTest"]'));

Przyciski typu checkbox umożliwiają wybór równocześnie kilku opcji.
Aby sprawdzić, czy dany checkbox jest zaznaczony, tak samo jak przy radio musimy sprawdzić jego właściwość checked, np:


if (document.querySelector('#someCheckbox').checked) {
    alert("zaznaczony!");
}

Najczęściej jednak checkboxów będzie kilka o takiej samej nazwie, więc zastosujemy selektor querySelectorAll wraz z pętlą:


function checkCheckbox(e) {
    e.preventDefault();

    const form = this;

    let checkedCount = 0;
    const checkbox = form.querySelectorAll(':scope input[name="color"]');

    for (let x=0; x<checkbox.length; x++) {
        if (checkbox[x].checked) {
            checkedCount++;
        }
    }

    if (checkedCount < 2) {
        alert('Wybrałeś ' +checkedCount+' owoce, a musisz wybrać 2');
    } else {
        alert('Wybrałeś ' +checkedCount+' owoce. Bądź pozdrowiony!');
    }
}

document.addEventListener("DOMContentLoaded", function() {
    document.querySelector('#myForm').addEventListener('submit', checkCheckbox);
});
Wybierz dwa (2) ulubione owoce
niebieski
czerwony
zielony
pomarańcz

Jeżeli w nazwie zastosujemy kwadratowe nawiasy, wysłane przez formularz wartości naszych zaznaczonych pól będą dostępne w PHP w postaci tablicy:


<input type="checkbox" name="hobby[]" value="rower"> rower<br />
<input type="checkbox" name="hobby[]" value="kino"> kino<br />
<input type="checkbox" name="hobby[]" value="komputer"> komputer<br />
<input type="checkbox" name="hobby[]" value="rysowanie"> rysowanie<br />

W powyższym przykładzie odwołanie się poprzez nazwę (name) nie będzie możliwe. Metoda getElementById też odpada, gdyż nasze elementy musiały by mieć identyczne ID, co jest niedopuszczalne. Aby odwołać się do tak nazwanych elementów trzeba zastosować konstrukcję:


    const hobby = document.querySelectorAll('#myForm name="hobby[]"');

Pola typu SELECT


<select name="eyeColor">
    <option value="-1">wybierz kolor</option>
    <option value="niebieskie">niebieskie</option>
    <option value="zielone">zielone</option>
    <option value="bronzowe">bronzowe</option>
    <option value="czarne">czarne</option>
</select>

Żeby pobrać długość selekta (ilość jego elementów) skorzystamy z właściwości length lub wykorzystamy kolekcję options, która zawiera wszystkie optiony danego selekta:


const select = document.querySelector('select[name="eyeColor"]');
const optionsLength = select.length

//lub

const select = document.querySelector('select[name="eyeColor"]');
const optionsLength = select.options.length

Aby sprawdzić który element został wybrany skorzystamy z właściwości selectedIndex:


const select = document.querySelector('select[name="eyeColor"]');
console.log(select.selectedIndex); //wypisze numer wybranego indeksu

Aby sprawdzić wartość value wybranego optiona skorzystamy z instrukcji:


const select = document.querySelector('select[name="eyeColor"]');
console.log( select.options[select.selectedIndex].value );

Przykładowo przy zmianie (change) selekta wypiszmy jego aktualną wartość:


const select = document.querySelector('select[name="eyeColor"]');
select.addEventListener('change', function() {
    alert('Wybrałeś: ' + this.options[this.selectedIndex].value);
});

Nowe option dla selektów

Javascript daje także możliwość tworzenie nowych pól option. Tak samo jak przy tworzeniu nowych obiektów skorzystamy tutaj z instrukcji new:


const optionText = "zaznacz mnie";
const optionValue = "opcja";

const newOption = new Option(optionText, optionValue);
const select = document.querySelector('select[name="eyeColor"]');
select.options[x] = newOption;

Tworząc nowy obiekt option musimy podać dla niego właściwości:

  • tekst - czyli tekst dla danego option
  • value - wartość danego option
  • defaultSelected - dodatkowy parametr - wskazuje, czy dany elemeny jest domyślnie wybrany
  • selectedIndex - dodatkowy parametr - wskazuje czy dany element jest wybrany. W polach select dla którego nie ustawiliśmy atrybutu multiply="multiply" wskazuje index wybranego elementu. Dla pól z atrybutem multiply wskazuje index tylko pierwszego wybranego elementu. W takim przypadku musimy wykonać pętlę po wszystkich elementach danego selecta i sprawdzać każdy element z osobna.

Jeżeli dodamy element o indeksie który już istnieje, to stary element zostanie zastąpiony. Jeżeli dodamy element o indeksie nr 10, a lista ma ich tylko 5, to powstaną puste, niezdefiniowane pola, a na miejscu 10 nowy element (w FireFoxie można definiować tylko elementy które są kolejnymi elementami dla SELECTA).

Dodawanie nowych elementów - szczególnie z wykorzystaniem pętli jest bardzo wygodną metodą tworzenia elementów typu select.

Przykład zastosowania pętli do konstruowania select możesz zobaczyć w formularzu wyboru daty.

Dynamicznie wypełniany selekt

Stwórzmy selecta, który będzie zawierał wybrane elementy innego selecta. Aby wykonać to zadanie, wykonujemy pętlę po elementach option pierwszego selecta. Gdy dany element option jest zaznaczony (selected), wówczas tworzymy w drugim selekcie nowy option, który ma tekst i value takie same jak dany element w pierwszym selekcie.


<form>
    <select id="firstSelect" multiple="multiple">
        <option value="1"> opcja 1 </option>
        <option value="2"> opcja 2 </option>
        <option value="3"> opcja 3 </option>
    </select>

    <select id="secondSelect" multiple="multiple">
    </select>

    <button type="button" id="copySelect">Kopiuj zaznaczone elementy option</button>
</form>

function moveOptions() {
    const select1 = document.querySelector('#firstSelect');
    const select2 = document.querySelector('#secondSelect');

    if (select1.length > 0) {
        for (let i=0; i<select1.length; i++){
            if (select1.options[i].selected) {
                var newOption = new Option(select1.options[i].firstChild.nodeValue, select1.options[i].value);
                select2[select2.length] = newOption;
            }
        }
    }
}

document.addEventListener("DOMContentLoaded", function() {
    document.querySelector('#copySelect').addEventListener('click', function(){
        moveOptions();
    });
});

Jeżeli byśmy chcieli dodatkowo z lewego selekta usuwać przerzucone wartości, tuż po ich skopiowaniu elementy te powinniśmy usunąć - tak samo jak usuwamy każdy inny element na stronie:


if (select1.length > 0) {
    for (let i=select1.length-1; i>=0; i--){
        if (select1.options[i].selected) {
            const newOption = new Option(select1.options[i].firstChild.nodeValue, select1.options[i].value);
            select2[select2.length] = newOption;
            select1.options[i].parentElement.removeChild(select1.options[i]);
        }
    }
}

Pętlę robimy od końca do początku, bo gdy usuniemy dany element, zmienią się liczniki w kolekcji.