Uwaga! Trwają prace nad nową wersją serwisu. Mogą występować przejściowe problemy z jego funkcjonowaniem. Przepraszam za niedogodności!

🏆 Lubisz podcast Pierwsze kroki w IT? Oddaj swój głos w rankingu: TOP 10 polskich podcastów o IT! 🏆

Tworzenie i walidacja formularza JavaScript

Stwórz elastyczne i reużywalne rozwiązanie!

Koniec długich instrukcji warunkowych, koniec poprawiania danych pól formularza osobno w JavaScripcie i HTML-u. Z tego artykułu dowiesz się, jak wykorzystać tablicę obiektów w JS do automatycznego generowania formularza oraz jak dzięki temu zaimplementować jego walidację.

Spis treści

 Pola formularza – tablica obiektów

Do tej pory zapewne przygotowywałeś formularz w pliku HTML, następnie wyszukiwałeś go w drzewie DOM w pliku JS i tam już obsługiwałeś dane wprowadzone przez użytkownika.

Rodziło to jednak co najmniej jeden problem – jeżeli w pliku HTML zmieniła się np. nazwa pola, musiałeś zmienić ją także w JavaScripcie. Dwa miejsca to już o jedno za dużo. Łatwo o błędy.

Przygotujmy więc „jedno miejsce”, w którym będziemy przechowywać wszystkie dane o polach formularza – nie tylko ich nazwy, ale również elementy potrzebne do walidacji.

Kod przedstawionego rozwiązania znajdziesz w repozytorium, a działanie na żywo podejrzysz dzięki GitHub Pages.

Mamy już przygotowany plik HTML z elementem body oraz podpiętym skryptem app.js (jeśli nie wiesz, jak ten plik powinien wyglądać, to zajrzyj do pliku HTML w repozytorium). Teraz zdefiniujmy dane w tablicy obiektów o nazwie fields. Każdy obiekt w tej tablicy reprezentuje jedno pole formularza i zawiera informacje takie jak: nazwa pola; etykieta; to, czy jest wymagane; wzorzec do walidacji (pattern) oraz typ pola. Typ pola nie występuje wszędzie. Gdy go nie ma, uznajemy, że jest to pole tekstowe.

// Czekam na wczytanie kodu HTML i dopiero wtedy wykonuję JS
document.addEventListener('DOMContentLoaded', init);

function init() {
  // Definiuję wszystkie niezbędne dane dla pól formularza
  const fields = [
    {
      name: 'firstName',
      label: 'Imię',
      required: true,
      pattern: '^[a-zA-Z –-]+$',
    },
    {
      name: 'lastName',
      label: 'Nazwisko',
      required: true,
      pattern: '^[a-zA-Z –-]+$',
    },
    { name: 'street', label: 'Ulica', required: true },
    {
      name: 'houseNumber',
      label: 'Numer budynku',
      type: 'number',
      required: true,
    },
    { name: 'flatNumber', label: 'Numer mieszkania', type: 'number' },
    {
      name: 'zip',
      label: 'Kod pocztowy',
      pattern: '^[0-9]{2}-[0-9]{3}$',
      required: true,
    },
    {
      name: 'city',
      label: 'Miasto',
      required: true,
      pattern: '^[a-zA-Z –-]+$',
    },
    { name: 'voivodeship', label: 'Województwo', required: true },
    {
      name: 'mobileNumber',
      label: 'Numer telefonu komórkowego',
      type: 'number',
      pattern: '^[1-9]{9}$',
      required: true,
    }
  ];
  //...
}

 Generowanie formularza z poziomu JavaScriptu

 Element formularza

Zanim stworzymy pola, warto mieć formularz. Tworzymy go za pomocą metody document.createElement(). Od razu dodajemy do niego nasłuchiwanie na zdarzenie 'submit' oraz tymczasowo atrybut 'novalidate', by przeglądarka „nie przeszkadzała” w sprawdzaniu działania naszej walidacji. Umieszczamy go w drzewie DOM w body, aby od razu widzieć efekty kolejnych działań.

// Tworzę element formularza
const form = document.createElement('form');

// Przypisuję do niego nasłuchiwanie na event 'submit'
// Po submicie uruchomi się funkcja handle Submit
form.addEventListener('submit', handleSubmit);

// Tymczasowo dodaję atrybut 'novalidate', by przeglądarka „nie przeszkadzała” mi w sprawdzaniu działania mojej własnej walidacji
form.setAttribute('novalidate', '');

// Dodaję cały formularz do elementu body
document.body.appendChild(form);

 Pola formularza

Czas na rozwiązanie, dzięki któremu będziemy mogli rozszerzać formularz o nowe pola i w ogóle nie martwić się o resztę implementacji – po prostu dodamy wówczas nowy obiekt do tablicy fields. Zrobimy to dla przykładu na końcu tego podpunktu.

Aby dla każdego obiektu tablicy fields stworzyć odpowiedni element HTML, należy po tej tablicy przeiterować. Użyjemy więc metody forEach(). Tworzę etykietę oraz input, a następnie dodaję do inputa odpowiednie atrybuty.

// Iteruję po elementach tablicy 'field' i dla każdego z nich tworzę element HTML
fields.forEach(function (field) {
// Tworzę element label z adekwatną nazwą "pobraną" z obiektu reprezentującego dane pole
const label = document.createElement('label');
label.innerText = field.label;

// Tworzę element input
const fieldEl = document.createElement('input');

// Do elementu input dodaję atrybuty wskazane w obiekcie
// Jeśli w obiekcie nie ma informacji o danym atrybucie, ustawiam wartość domyślną dzięki operatorowi 'or', czyli ||
fieldEl.setAttribute('type', field.type || 'text');
fieldEl.setAttribute('name', field.name);
fieldEl.setAttribute('required', field.required || false);
if (field.pattern) {
  fieldEl.setAttribute('pattern', field.pattern);
}

// Dodaję input do elementu label
label.appendChild(fieldEl);

// Opcjonalnie na potrzeby stylowania:
// tworzę div i dodaję do niego label
// const div = document.createElement('div');
// div.appendChild(label);

// Dodaję label (lub div) do formularza
form.appendChild(label);

Zwróć uwagę, że w tej chwili nie mamy możliwości stworzenia elementu textarea. Niech to będzie nasz dodatkowy obiekt, o którym wspominam na początku tego punktu. Dla przykładu stwórzmy pole z opisem. Do reprezentującego go obiektu dodajmy właściwość o nazwie htmlEl z wartością 'textarea':

const fields = [
  //...
  {
    name: 'description',
    label: 'Temat rozmowy',
    required: true,
    htmlEl: 'textarea',
  }
];

Wystarczy dodać obsługujący tę opcję operator warunkowy:

// Iteruję po elementach tablicy 'field' i dla każdego z nich tworzę element HTML
fields.forEach(function (field) {
  // Tworzę element label z adekwatną nazwą "pobraną" z obiektu reprezentującego dane pole
  const label = document.createElement('label');
  label.innerText = field.label;

  // Tworzę pole formularza: input lub textarea
  const fieldEl =
    field.htmlEl === 'textarea'
      ? document.createElement('textarea')
      : document.createElement('input');
  //...
});

Tworzenie pól formularza jest gotowe. Teraz dodaj własny obiekt z danymi dowolnego pola i sprawdź, czy nasza implementacja działa.

 Przycisk „wyślij” (submit)

Użytkownik nie wyśle nam wprowadzonych informacji, jeśli nie będzie mieć przycisku. Już poza metodą forEeach() tworzymy i dodajemy button "Wyślij". Gdy wszystkie elementy formularza są gotowe, wstawiamy go do drzewa DOM.

Implementujemy też funkcję obsługującą zdarzenie 'submit' formularza, czyli funkcję handleSubmit(). W niej wykonamy walidację formularza.

// Tworzę przycisk 'Submit'
const submitButton = document.createElement('button');
submitButton.type = 'submit';
submitButton.innerText = 'Wyślij';

// Dodaję przycisk do formularza
form.appendChild(submitButton);

Teraz automatyczne tworzenie formularza jest ukończone. Możesz przenieść ten kod np. do osobnej klasy i wykorzystywać ją w innych projektach – wystarczy odpowiednie zdefiniowanie obiektów w tablicy i gotowe: formularz wygeneruje się sam.

 Walidacja formularza w JavaScript

 Wyświetlanie komunikatów o błędach na liście ul

Będziemy teraz wyświetlać komunikaty o błędach na liście ul. Najpierw musimy ją stworzyć:

 // Tworzę i dodaję do elementu body listę <ul>, która przyda się do wyświetlania użytkownikowi błędów
const ulEl = document.createElement('ul');
document.body.appendChild(ulEl);

// Tworzę element formularza
const form = document.createElement('form');

// ...

W funkcji handleSubmit() ponownie iterujemy po tablicy fields i sprawdzamy, czy wartości wprowadzone przez użytkownika spełniają wymagane kryteria.

// Obsługuję podsumowanie formularza (po kliknięciu „Wyślij”)
function handleSubmit(event) {
  // Nie wysyłam formularza, dzięki czemu mogę sprawdzić poprawność wprowadzonych przez użytkownika danych po stronie front endu
  event.preventDefault();

  const errors = [];

  fields.forEach(function (field) {
    // wykorzystuję właściwość obiektu o nazwie name, by pobrać wartość konkretnego elementu formularza
    const value = form.elements[field.name].value;

    if (field.required) {
      if (value.length === 0) {
        errors.push('Dane w polu ' + field.label + ' są wymagane.');
      }
    }

    if (field.type === 'number') {
      if (Number.isNaN(Number(value))) {
        errors.push('Dane w polu ' + field.label + ' muszą być liczbą.');
      }
    }

    if (field.pattern) {
      const reg = new RegExp(field.pattern);
      if (!reg.test(value)) {
        errors.push(
          'Dane w polu ' +
            field.label +
            ' zawierają niedozwolone znaki lub nie są zgodne z przyjętym w Polsce wzorem.'
        );
      }
    }
  });

  // czyszczę listę błędów
  ulEl.innerHTML = '';
  if (errors.length === 0) {
    alert('Dane zostały wypełnione prawidłowo!');

    // czyszczę pola po prawidłowym wypełnieniu formularza
    fields.forEach(function (el) {
      form[el.name].value = '';
    });
  } else {
    errors.forEach(function (text) {
      const liEl = document.createElement('li');
      liEl.innerText = text;

      ulEl.appendChild(liEl);
    });
  }
}

W przypadku, gdy pole jest oznaczone jako wymagane, sprawdzamy, czy zostało wypełnione. Jeśli nie, dodajemy komunikat o błędzie do listy błędów. Podobnie postępujemy, gdy pole ma określony typ. My wykorzystujemy tylko typ 'number', lecz nic nie stoi na przeszkodzie, abyś rozbudował walidację o inne typy (np. 'email'). W przypadku pola 'number' sprawdzamy, czy wartość wprowadzona przez użytkownika jest liczbą. Jeśli nie, dodajemy odpowiedni komunikat do listy błędów.

Jeśli w polu formularza został wprowadzony wzorzec (pattern), to sprawdzamy, czy wartość pasuje do wzorca (wyrażenia regularnego). Jeśli nie pasuje, to oczywiście dodajemy błąd do listy errors.

Po przejściu przez wszystkie pola sprawdzamy, czy lista błędów jest pusta. Jeśli tak, oznacza to, że formularz został wypełniony poprawnie – należy wyczyścić listę błędów (jeśli do tej pory jakieś były, to trzeba się ich pozbyć) i można już przeprowadzić dalsze operacje, takie jak np. wysłanie danych na serwer czy dodanie taska do tablicy kanban.

W przeciwnym przypadku, czyli gdy tablica errors nie jest pusta, wyświetlamy błędy na stronie na naszej liście ul.

Kod przedstawionego rozwiązania znajdziesz w repozytorium, a działanie na żywo podejrzysz dzięki GitHub Pages.

 Wyświetlanie komunikatu o błędzie pod polem formularza

Nasze rozwiązanie jest teraz na tyle elastyczne, że możemy z łatwością zmienić koncepcję wyświetlania błędów. Wykorzystamy do tego fakt, że w obiektach w tablicy fields mamy wszystkie potrzebne dane.

Błędy będziemy wyświetlać pod polami formularza. Nasz kod nieznacznie się zmieni. Zamiast umieszczać komunikat w tablicy, dodamy go jako tekst do elementu HTML, np. small, a następnie element ten wrzucimy do etykiety (czyli umieścimy go jako jej następne dziecko, zatem pod polem formularza).

if (field.required) {
  if (value.length === 0) {
    // Tworzę element small, który będzie zawierał komunikat o błędzie
    const errorEl = document.createElement("small");
    errorEl.textContent = 'Dane w polu ' + field.label + ' są wymagane.'
    // Dodaję go do elementu label (label jest u nas rodzicem - parentNode - dla pola formularza)
    form.elements[field.name].parentNode.appendChild(errorEl);
  }
}

Teraz Twoim zadaniem będzie dostosowanie w ten sam sposób reszty warunków.

OK, jest jeszcze kwestia usuwania błędów, gdy pole zostaje wypełnione prawidłowo. Do tej pory po prostu czyściliśmy tablicę errors, lecz nie możemy już z niej skorzystać (możesz usunąć kod z nią powiązany). Teraz będziemy pozbywać się elementów small na początku instrukcji wewnątrz funkcji handleSubmit():

// Obsługuję podsumowanie formularza (po kliknięciu „Wyślij”)
function handleSubmit(event) {
  // Nie wysyłam formularza, dzięki czemu mogę sprawdzić poprawność wprowadzonych przez użytkownika danych po stronie front endu
  event.preventDefault();

  const errorEls = document.querySelectorAll('small');
  if (errorEls.length !== 0) {
    errorEls.forEach(el => el.remove());
  }
  //...
}

W ten sposób usuniemy „stare” komunikaty, a nowe wyświetlą się w wyniku wykonania reszty kodu.

Uwaga: usuwanie wszystkich elementów small z drzewa DOM nie jest dobrym rozwiązaniem. Lepiej będzie nadać im odpowiednią klasę i po tej klasie wyszukiwać. Ja tego nie zrobiłam, by ułatwić przekaz.

Walidacja formularza również jest na tyle elastyczna, że możemy wykorzystać ją gdzie indziej. W ramach nauki możesz stworzyć z niej klasę czy funkcję eksportowaną z osobnego pliku i spróbować wykorzystać w innym swoim projekcie.

 

Dynamiczne tworzenie elementów formularza oraz jego walidacja jest już często dostępna np. w ramach dodatkowych bibliotek. Warto jednak poznać podstawy i wiedzieć, co dzieje się „pod spodem”. Nigdy nie wiadomo, kiedy będzie trzeba np. dopisać własne rozwiązanie do gotowca lub stworzyć od zera krótki formularz, do którego nie będzie się opłacało importować biblioteki.

Udostępnij ten artykuł:

Mentoring to efektywna nauka pod okiem doświadczonej osoby, która:

  • przekazuje Ci swoją wiedzę i nadzoruje Twoje postępy w zdobywaniu umiejętności,
  • uczy Cię dobrych praktyk i wyłapuje złe nawyki,
  • wspiera Twój rozwój i zwiększa zaangażowanie w naukę.