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 minimum dwa 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 wartość danego pola. Każde pole może mieć atrybut o podobnej nazwie, więc pomyślisz, że powinieneś użyć getAttribute('value'). Byłby to jednak błąd, gdyż getAttribute zwraca wartość atrybutu, jaki jest wpisana w html. Wartość value zwraca dynamiczną wartość, jaką użytkownik wpisze w dane pole.


//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 input = document.querySelector("#name");
const val = input.value;

if (!val.length) {
    console.log('Nie wpisałeś żadnej wartości!')
} else {
    console.log('Twój imię to: ' + val[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>

const input = document.querySelector('#userName');
input.addEventListener('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[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>

const input = document.querySelector('#name');
input.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ę klasę do pola
    } else {
        this.classList.remove(className); //usuwam klasę
    }
});

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


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:


const input = document.querySelector('#email');
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ę klasę 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ę oraz indywidualną wartość.


<fieldset>
    <legend>Wybierz płeć</legend>
    <input type="radio" value="male" name="gender" /> mężczyzna
    <input type="radio" value="female" name="gender" /> kobieta
</fieldset>

function checkRadio() {
    const genderRadio = this.form.querySelectorAll('input[name="gender"]');
    for (const radio of genderRadio) {
        if (radio.checked) {
            alert('Zaznaczyłeś opcję: \n' + radio.value);
            break;
        }
    }
}

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

    for (const radio of genderRadio) {
        radio.addEventListener('click', checkRadio);
    }
});
Wybierz płeć mężczyzna kobieta

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 null. Następnie wykonując pętlę sprawdzamy, czy któreś pole jest zaznaczone. Jeżeli tak, naszej zmiennej value zaznaczonego pola:


let selectedValue = null;
const genderRadio = document.querySelectorAll('#myForm input[name="gender"]);

for (const radio of genderRadio) {
    if (radio.checked) {
        selectedValue = radio.value;
        break;
    }
}

if (checkedIndex !== null) {
    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ą:


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

for (const chk of checkboxes) {
    if (chk.checked) {
        checkedCount++;
    }
}

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

Pola typu SELECT


<select name="eyeColor">
    <option value="-1">wybierz kolor</option>
    <option value="niebieski">niebieski</option>
    <option value="zielony">zielony</option>
    <option value="brązowy">brązowy</option>
    <option value="czarny">czarny</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 option został wybrany skorzystamy z właściwości selectedIndex:


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

Aby sprawdzić wartość selekta skorzystamy z instrukcji:


console.log(select.value);

//lub ze zmyłką by zaimponować dziewczynie

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.value);
});

Jak widzisz powyżej, właściwości selectedIndex nie ma co używać do pobierania wartości selekta, ale czasami przy operacjach na samych optionach sprawdza się ona bardzo dobrze.

Wyobraź sobie, że masz selekt, który ma w sobie optiony z tekstem zaczynającym się na podobne litery.


<select name="citySelect" id="citySelect">
    <option value="1">Płock</option>
    <option value="2">Poznań</option>
    <option value="3">Pabianice</option>
    <option value="4">Warszawa</option>
    <option value="5">Węgorzewo</option>
    <option value="6">Wrocław</option>
    <option value="7">Zabrze</option>
    <option value="8">Zakopane</option>
    <option value="9">Zamość</option>
</select>

Po wybraniu danej opcji, reszta rozpoczynająca się na daną literę powinna być nieaktywna. Niestety nie możesz tutaj użyć value optionów, bo wskazują na id danego elementu.


select.addEventListener("change", function() {
    const val = this.value;
    const text = this.options[this.selectedIndex].innerText;

    for (const opt of this.options) {
        if (opt.innerText[0].toUpperCase() === text[0].toUpperCase()) {
            opt.disabled = true;
        } else {
            opt.disabled = false;
        }
    }
});

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 select = document.querySelector('select[name="eyeColor"]');

const optionText = "zaznacz mnie";
const optionValue = "wartość optiona";
const newOption = new Option(optionText, optionValue);
select.options[x] = newOption;

//to samo możemy uzyskać bardziej klasycznie
const newOption = document.createElement("option");
newOption.innerText = "zaznacz mnie";
newOption.value = "wartość optiona";

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

  • tekst - czyli tekst dla danego option
  • value - wartość danego option
  • defaultSelected - opcjonalny parametr - wskazuje, czy dany element jest domyślnie wybrany
  • selectedIndex - opcjonalny 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.

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.