Formularz kontaktowy
Poniższy tekst będzie w zasadzie podsumowaniem tego, co sobie powiedzieliśmy w poprzednich rozdziałach. Naszym celem będzie stworzenie w pełni działającego dynamicznego formularza kontaktowego.
HTML i CSS
Zaczynamy od prostego kodu HTML i CSS, który przewijał nam się już w poprzednich dyskusjach:
Pokaż HTMLPokaż CSS
Wygląd formularza możesz zobaczyć tutaj.
Do naszego formularza wybierzemy sposób z umieszczeniem komunikatów błędów w dataset pól.
Tak jak to robiliśmy w tamtym rozdziale - każde wymagane pole w naszym formularzu będzie miało dodatkowy atrybut data-error-text
, który zawiera tekst wyświetlany w razie błędu. W razie jeżeli nie wiesz o czym mówimy, zapraszam do lektury.
Początkowy skrypt
Żeby na siłę nie powtarzać czynności, zacznijmy od punktu, na którym skończyliśmy w poprzednim rozdziale, to jest na podpięciu sprawdzania pól w czasie pisania, ale i przy próbie wysyłki formularza:
function removeFieldError(field) {
const errorText = field.nextElementSibling;
if (errorText !== null) {
if (errorText.classList.contains("form-error-text")) {
errorText.remove();
}
}
};
function createFieldError(field, text) {
removeFieldError(field); //przed stworzeniem usuwam by zawsze był najnowszy komunikat
const div = document.createElement("div");
div.classList.add("form-error-text");
div.innerText = text;
if (field.nextElementSibling === null) {
field.parentElement.appendChild(div);
} else {
if (!field.nextElementSibling.classList.contains("form-error-text")) {
field.parentElement.insertBefore(div, field.nextElementSibling);
}
}
};
function toggleErrorField(field, show) {
const errorText = field.nextElementSibling;
if (errorText !== null) {
if (errorText.classList.contains("form-error-text")) {
errorText.style.display = show ? "block" : "none";
errorText.setAttribute('aria-hidden', show);
}
}
};
function markFieldAsError(field, show) {
if (show) {
field.classList.add("field-error");
} else {
field.classList.remove("field-error");
toggleErrorField(field, false);
}
};
//pobieram elementy
const form = document.querySelector("#contactForm");
const inputs = form.querySelectorAll("[required]");
form.setAttribute("novalidate", true);
//etap 1 : podpinam eventy
for (const el of inputs) {
el.addEventListener("input", e => markFieldAsError(e.target, !e.target.checkValidity()));
}
form.addEventListener("submit", e => {
e.preventDefault();
let formErrors = false;
//2 etap - sprawdzamy poszczególne pola gdy ktoś chce wysłać formularz
for (const el of inputs) {
removeFieldError(el);
el.classList.remove("field-error");
if (!el.checkValidity()) {
createFieldError(el, el.dataset.errorText);
el.classList.add("field-error");
formHasErrors = true;
}
}
if (!formErrors) {
//form.submit();
//dane będziemy wysyłać dynamicznie!
}
});
Sprawdź formularz w akcji
Wysyłka formularza
W tym momencie możemy już wysłać formularz, a i nasza walidacja po stronie przeglądarki działa.
Żeby wysłać dane, musimy pobrać wszystkie dane z formularza. Wystarczy zrobić pętlę po elementach formularza i zebrać je pod postacią formData:
...
form.addEventListener("submit", e => {
e.preventDefault();
let formErrors = false;
//2 etap - sprawdzamy poszczególne pola gdy ktoś chce wysłać formularz
for (const el of inputs) {
markFieldAsError(el, false);
toggleErrorField(el, false);
if (!el.checkValidity()) {
markFieldAsError(el, true);
toggleErrorField(el, true);
formErrors = true;
}
}
if (!formErrors) {
//tutaj wysyłka
const formData = new FormData(form);
}
});
Mamy zebrane dane. Pozostaje je wysłać.
Przed samym wysłaniem danych wyłączmy przycisk submit poprzez dodanie do niego atrybutu disabled
. Dzięki temu zniecierpliwiony użytkownik nie będzie mógł klikać w przycisk wysyłania aż do czasu zakończenia poprzedniej wysyłki.
Dodatkowo powinniśmy pokazać jakiś wskaźnik wczytywania. Możemy to zrobić jakąś graficzką loadingu, ale lepiej zastosować jakąś prostą animację:
.loading {
position: relative;
pointer-events: none;
opacity:0.5;
}
.loading::after {
position: absolute;
left: 50%;
top: 50%;
width: 20px;
height: 20px;
border-radius: 50%;
border: 2px solid rgba(0, 0, 0, 0.2);
border-right-color: rgba(0,0,0,0.7);
transform: translate(-50%, -50%) rotate(0deg);
content:"";
animation: rotateSingleLoading 0.3s infinite linear;
z-index: 10;
}
@keyframes rotateSingleLoading {
from {
transform: translate(-50%, -50%) rotate(0deg);
}
to {
transform: translate(-50%, -50%) rotate(360deg);
}
}
const loadingTest = document.querySelector("#loadingTest")
loadingTest.addEventListener("click", e => {
loadingTest.classList.add("loading");
loadingTest.disabled = true;
});
Zastosujmy ją w naszym skrypcie:
form.addEventListener("submit", e => {
e.preventDefault();
let formErrors = false;
//2 etap - sprawdzamy poszczególne pola gdy ktoś chce wysłać formularz
for (const el of inputs) {
markFieldAsError(el, false);
toggleErrorField(el, false);
if (!el.checkValidity()) {
markFieldAsError(el, true);
toggleErrorField(el, true);
formErrors = true;
}
}
if (!formErrors) {
const submit = form.querySelector("[type=submit]");
submit.disabled = true;
submit.classList.add("loading");
...
}
});
Do samej wysyłki zastosujemy fetch. Dla adresu i metody jaką wyślemy musimy pobrać atrybuty action i method z formularza. Wcześniej pobrane dane z formularza wysyłamy jako POST, dlatego zamieniamy je za pomocą formData na właściwy zapis:
form.addEventListener("submit", e => {
e.preventDefault();
let formErrors = false;
//2 etap - sprawdzamy poszczególne pola gdy ktoś chce wysłać formularz
for (const el of inputs) {
markFieldAsError(el, false);
toggleErrorField(el, false);
if (!el.checkValidity()) {
markFieldAsError(el, true);
toggleErrorField(el, true);
formErrors = true;
}
}
if (!formErrors) {
const submit = form.querySelector("[type=submit]");
submit.disabled = true;
submit.classList.add("loading");
//generuję dane do wysyłki
const formData = new FormData(form);
const url = form.getAttribute("action"); //pobieramy adres wysyłki z action formularza
const method = form.getAttribute("method"); //tak samo metodę
fetch(url, {
method: method,
body: formData
})
.then(res => res.json())
.then(res => {
//tutaj odpowiedź
}).finally(() => { //gdy zakończy się połączenie chcemy włączyć przycisk submit
submit.disabled = false;
submit.classList.remove("loading");
});
}
});
Po stronie serwera
Formularz wysłaliśmy na serwer. Czas zająć się napisaniem skryptu serwerowego. Do naszych celów użyjemy PHP, ale podobne działanie można utworzyć w każdej innej technologii serwerowej:
<?php
$mailToSend = "twoj-mail@lorem.pl";
if ($_SERVER["REQUEST_METHOD"] === "POST") {
$name = $_POST["name"];
$email = $_POST["email"];
$message = $_POST["message"];
$errors = [];
$return = [];
if (empty($name)) { //jeżeli pusta wartość
array_push($errors, "name");
}
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { //sprawdzamy czy email ma zły wzór
array_push($errors, "email");
}
if (empty($message)) {
array_push($errors, "message");
}
if (count($errors) > 0) {
$return["errors"] = $errors;
} else {
//każde wysłanie wiadomości musi być poprzedzone ustawieniem nagłówków
$headers = "MIME-Version: 1.0" . "\r\n";
$headers .= "Content-type: text/html; charset=UTF-8". "\r\n";
$headers .= "From: ".$email."\r\n";
$headers .= "Reply-to: ".$email;
$message = "
<html>
<head>
<meta charset=\"utf-8\">
</head>
<body>
<div> Imię: $name</div>
<div> Email: <a href=\"mailto:$email\">$email</a> </div>
<div> Wiadomość: </div>
<div> $message </div>
</body>
</html>";
if (mail($mailToSend, "Wiadomość ze strony - " . date("d-m-Y"), $message, $headers)) {
$return["status"] = "ok";
} else {
$return["status"] = "error";
}
}
header("Content-Type: application/json");
echo json_encode($return);
}
Jest to prosty skrypt, który kolejno sprawdza przesłane pola. Jeżeli dane są poprawne, skrypt spróbuje wysłać maila.
Zwrócone do Javascript dane mogą być w 3 postaciach:
{status: "ok"}
- gdy wszystko poszło dobrze i wiadomość została wysłana{status: "error"}
- dane z formularza były poprawne, ale z jakiegoś powodu nie udało się wysłać wiadomości{"errors" : ["name", "email", "message"] }
- tablica zawierająca nazwy pól, które zostały wysłane z formularza z błędnymi danymi
Czasami mogą się pojawić problemy z wysyłką maila z naszego serwera. Może to być spowodowane wieloma czynnikami. Niektóre serwery wymagają by nagłówek From
wskazywał na maila, który mamy podpięty pod dany serwer. Inne wymagają dodatkowych parametrów.
A i nie zawsze będziemy korzystać przecież z PHP. Może w naszym przypadku lepiej sprawdzi się nodemailer? A nawet czasami nie potrzeba w ogóle skryptów na serwerze. Zawsze możemy skorzystać z usług darmowych dostawców statycznych formularzy, które to serwisy wyślą takie maile za nas. Dobre zestawienie znajdziesz pod adresem https://css-tricks.com/a-comparison-of-static-form-providers/
Tutaj znajdziesz też wersję dla Node.js. Korzysta ona z express, oraz z NodeMailera.
Odpowiedź z serwera - błędne dane
Serwer dostał paczkę danych, obsłużył ją i zwraca nam odpowiedź. W pierwszej kolejności obsłużmy odpowiedź z wypisanymi błędnie wypełnionymi polami:
form.addEventListener("submit", e => {
e.preventDefault();
let formErrors = false;
//2 etap - sprawdzamy poszczególne pola gdy ktoś chce wysłać formularz
for (const el of inputs) {
markFieldAsError(el, false);
toggleErrorField(el, false);
if (!el.checkValidity()) {
markFieldAsError(el, true);
toggleErrorField(el, true);
formErrors = true;
}
}
if (!formErrors) {
const submit = form.querySelector("[type=submit]");
submit.disabled = true;
submit.classList.add("loading");
const formData = new FormData(form);
const url = form.getAttribute("action");
const method = form.getAttribute("method");
fetch(url, {
method: method.toUpperCase(),
body: formData
})
.then(res => res.json())
.then(res => {
if (res.errors) { //błędne pola
const selectors = res.errors.map(el => `[name="${el}"]`);
const fieldsWithErrors = form.querySelectorAll(selectors.join(","));
for (const el of fieldsWithErrors) {
markFieldAsError(el, true);
toggleErrorField(el, true);
}
} else { //pola są ok - sprawdzamy status wysyłki
if (res.status === "ok") {
//wyświetlamy komunikat powodzenia, cieszymy sie
}
if (res.status === "error") {
//komunikat błędu, niepowodzenia
}
}
}).catch(() => {
submit.disabled = false;
submit.classList.remove("loading");
});
}
});
Gdy dostaniemy w odpowiedzi błędy w postaci zmiennej res.errors
, każdą wartość tej tablicy (która zawiera nazwy naszych pól) zmieniamy za pomocą map na teksty np. [name="email"]
czy [name="message"]
. Następnie taką tablicę łączymy za pomocą join(","), dzięki czemu uzyskujemy zapis:
const selectors = res.errors.map(el => `[name="${el}"]`);
const fieldsWithErrors = form.querySelectorAll(ret.errors.join(","));
//da nam w wyniku
const fieldsWithErrors = form.querySelectorAll(`[name="name"],[name="email"],[name="message"]...`)
Gdy już dostaniemy odpowiednie pola, podobnie jak na początku robimy po nich pętlę i pokazujemy im błędy (linie 38-40).
Odpowiedź z serwera - błąd serwera
Pozostały nam rzeczy małe, czyli obsługa odpowiedzi, gdy status równy jest "ok" i "error".
Zacznijmy od błędów. Dla error możemy dodać do formularza tekst z komunikatem błędu wysyłki. Gdzie? A to zależy od naszych upodobać - może być np. po prawej stronie przycisku wyślij (a może jakiś toast):
form.addEventListener("submit", e => {
e.preventDefault();
let formErrors = false;
//2 etap - sprawdzamy poszczególne pola gdy ktoś chce wysłać formularz
for (const el of inputs) {
markFieldAsError(el, false);
toggleErrorField(el, false);
if (!el.checkValidity()) {
markFieldAsError(el, true);
toggleErrorField(el, true);
formErrors = true;
}
}
if (!formErrors) {
const submit = form.querySelector("[type=submit]");
submit.disabled = true;
submit.classList.add("loading");
const formData = new FormData(form);
const url = form.getAttribute("action");
const method = form.getAttribute("method");
fetch(url, {
method: method.toUpperCase(),
body: formData
})
.then(res => res.json())
.then(res => {
if (res.errors) {
const selectors = res.errors.map(el => `[name="${el}"]`);
const fieldsWithErrors = form.querySelectorAll(selectors.join(","));
for (const el of fieldsWithErrors) {
markFieldAsError(el, true);
toggleErrorField(el, true);
}
} else {
if (res.status === "ok") {
//wyświetlamy komunikat powodzenia, cieszymy sie
}
if (res.status === "error") {
//jeżeli istnieje komunikat o błędzie wysyłki
//np. generowany przy poprzednim wysyłaniu formularza
//usuwamy go, by nie duplikować tych komunikatów
const statusError = document.querySelector(".send-error");
if (statusError) {
statusError.remove();
}
const div = document.createElement("div");
div.classList.add("send-error");
div.innerText = "Wysłanie wiadomości się nie powiodło";
submit.parentElement.appendChild(div);
}
}
}).finally(() => {
submit.disabled = false;
submit.classList.remove("loading");
});
}
});
Dodajmy też proste stylowanie dla tego komunikatu:
.form .send-error {
display:inline-block;
font-family: sans-serif;
padding:1rem 2rem;
color:red;
}
@media screen and (max-width:500px) {
.form .send-error {
text-align:center;
display: block;
}
}
Odpowiedź z serwera - wreszcie pozytywnie
Uff. Zajmijmy się teraz pozytywną odpowiedzią. Możemy tutaj oczywiście wstawić tekst tak samo jak w przypadku błędu serwera. Polecam jednak zastąpienie całego formularza html z informacją o pozytywnej wysyłce. Dzięki temu formularz będzie trochę bardziej odporny na różne ataki robotów lub ludzi, którzy lubią wysyłać formularze za pomocą setInterval:
form.addEventListener("submit", e => {
e.preventDefault();
let formErrors = false;
//2 etap - sprawdzamy poszczególne pola gdy ktoś chce wysłać formularz
for (const el of inputs) {
markFieldAsError(el, false);
toggleErrorField(el, false);
if (!el.checkValidity()) {
markFieldAsError(el, true);
toggleErrorField(el, true);
formErrors = true;
}
}
if (!formErrors) {
const submit = form.querySelector("[type=submit]");
submit.disabled = true;
submit.classList.add("loading");
const formData = new FormData(form);
const url = form.getAttribute("action");
const method = form.getAttribute("method");
fetch(url, {
method: method.toUpperCase(),
body: formData
})
.then(res => res.json())
.then(res => {
if (res.errors) {
const selectors = res.errors.map(el => `[name="${el}"]`);
const fieldsWithErrors = form.querySelectorAll(selectors.join(","));
for (const el of fieldsWithErrors) {
markFieldAsError(el, true);
toggleErrorField(el, true);
}
} else {
if (res.status === "ok") {
const div = document.createElement("div");
div.classList.add("form-send-success");
div.innerText = "Wysłanie wiadomości się nie powiodło";
form.parentElement.insertBefore(div, form);
div.innerHTML = `
<strong>Wiadomość została wysłana</strong>
<span>Dziękujemy za kontakt. Postaramy się odpowiedzieć jak najszybciej</span>
`;
form.remove();
}
if (res.status === "error") {
//jeżeli istnieje komunikat o błędzie wysyłki
//np. generowany przy poprzednim wysyłaniu formularza
//usuwamy go, by nie duplikować tych komunikatów
const statusError = document.querySelector(".form-send-error");
if (statusError) {
statusError.remove();
}
const div = document.createElement("div");
div.classList.add("form-send-error");
div.innerText = "Wysłanie wiadomości się nie powiodło";
submit.parentElement.appendChild(div);
}
}
}).finally(() => {
submit.disabled = false;
submit.classList.remove("loading");
});
}
});
Oraz dodajmy dla takiego komunikatu stylowanie:
.form-send-success {
font-family: sans-serif;
text-align:center;
font-size:2rem;
font-weight:bold;
border:1px solid #eee;
color:#333;
padding:10rem 0;
margin:3rem auto;
max-width:40rem;
}
.form-send-success strong {
display:block;
margin-bottom:0.5rem;
}
.form-send-success span {
font-size:1rem;
color:#888;
font-weight:normal;
display: block;
}
Mini bonus - słodki antyspam
Powyżej stworzyliśmy prosty w pełni dynamiczny formularz kontaktowy. Problem z takimi formularzami jest taki, że bardzo lubią je roboty rozsyłające spam. Aby im przeszkodzić musimy jakoś zabezpieczyć nasze dzieło. Jedną ze sprytniejszych metod jest tak zwana honeypot. Polega ona na dodaniu do formularza dodatkowego pola, które ukrywamy za pomocą styli:
<span class="form-row honey-row">
<label for="honey">Jeżeli jesteś człowiekiem, nie wypełniaj tego pola</label>
<input type="text" name="honey">
</span>
.form .honey-row {
display:none;
}
Większość robotów ignoruje style, ani nie rozumie języka pisanego, więc wypełnią to pole. Wystarczy teraz po stronie serwera sprawdzić czy pole to ma wartość. Jak ma, olewamy całą sprawę, ewentualnie pokrzepiając robota miłymi słowami wiadomości w formie sukcesu:
<?php
$mailToSend = "twoj-email@lorem.pl";
if ($_SERVER["REQUEST_METHOD"] === "POST") {
$name = $_POST["name"];
$email = $_POST["email"];
$message = $_POST["email"];
$antiSpam = $_POST["honey"];
$errors = Array();
$return = Array();
if (empty($antiSpam)) {
//tutaj cała reszta skryptu który mieliśmy wcześniej (od 23 do 49)
if (count($errors) > 0) {
$return["errors"] = $errors;
} else {
...
}
} else {
$return["status"] = "ok";
}
header("Content-Type: application/json");
echo json_encode($return);
}
Demo
Gotowy formularz znajdziesz poniżej:
demo