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
Początkowe kroki
Pierwsze czynności będą podobne jak w przypadku poprzednich 2 rozdziałów, czyli pobierzemy formularz, pola do walidacji, wyłączymy domyślne dymki walidacji, a następnie podepniemy odpowiednie zdarzenia:
const form = document.querySelector("#contactForm");
const fields = form.querySelectorAll("[required]");
const formMessage = form.querySelector(".form-message");
//wyłączam validationApi by nie przeszkadzało
form.setAttribute("novalidate", true);
//dodaję dynamiczną walidację pól
for (const field of fields) {
field.addEventListener("input", () => field.classList.toggle("is-invalid", !field.checkValidity()));
}
form.addEventListener("submit", e => {
...
});
Samą wysyłkę napiszemy jako oddzielną funkcję, która przed wysłaniem danych sprawdzi wszystkie pola:
//przed wysyłką sprawdzam pola
function checkRequiredFields() {
let formErrors = false;
for (const field of fields) {
if (!el.checkValidity()) {
field.classList.add("form-error");
formErrors = true;
} else {
field.classList.remove("form-error");
}
}
return formErrors;
}
//funkcja do wysyłania formularza
function submitForm(e) {
let formErrors = checkRequiredFields();
if (!formErrors) {
//form.submit();
//tym razem bez form.submit(). Dane będziemy wysyłać dynamicznie
}
}
form.addEventListener("submit", e => {
e.preventDefault();
submitForm()
});
Wysyłka formularza
Żeby wysłać dane, musimy pobrać wszystkie dane z formularza. Wystarczy do tego użyć formData:
...
function checkRequiredFields() {
...
}
function submitForm() {
let formErrors = checkRequiredFields();
if (!formErrors) {
const formData = new FormData(form);
}
}
form.addEventListener("submit", e => {
e.preventDefault();
submitForm()
});
Przed samym wysłaniem danych wyłączmy przycisk submit poprzez dodanie do niego atrybutu disabled i odpowiedniej klasy. Dzięki temu zniecierpliwiony użytkownik nie będzie mógł klikać w przycisk wysyłania aż do czasu zakończenia poprzedniej wysyłki. Tu możesz zobaczyć przykład.
async function submitForm() {
let formErrors = checkElements();
if (!formErrors) {
const formData = new FormData(form);
const submit = form.querySelector(".form-submit");
submit.disabled = true;
submit.classList.add("loading");
//...tutaj dynamiczna wysyłka
submit.disabled = false;
submit.classList.remove("loading");
}
}
Dane chcemy wysyłać dynamicznie bez przeładowania strony. Zastosujemy do tego fetch. Napiszmy do tego oddzielną funkcję:
const url = "https://skrypt-na-serwerze.php";
...
async function makeRequest(data) {
const res = await fetch(url, {
method : "post",
body: data
});
if (res.ok) {
return res.json();
}
return Promise.reject(`${res.status}: ${res.statusText}`);
}
async function submitForm() {
let formErrors = checkElements();
if (!formErrors) {
const formData = new FormData(form);
const submit = form.querySelector("[type=submit]");
submit.disabled = true;
submit.classList.add("loading");
try {
const response = await makeRequest(formData);
//tutaj odpowiedź
} catch(err) {
//tutaj błędy
}
submit.disabled = false;
submit.classList.remove("loading");
}
}
Po stronie serwera
Formularz wysłaliśmy na serwer. 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"] = "success";
} 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: "success"}- 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
Serwer dostał dane, obsłużył je i zwraca nam odpowiedź w 3 wariantach.
Gdy w odpowiedzi dostaniemy zmienną errors, zakładamy, że będzie to tablica z nazwami błędnie wypełnionych pól. Każdą wartość tej tablicy (linie 17-18) zmieniamy za pomocą map na tablicę selektorów CSS np. [name="email"], [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(","));
//powyższe jest równoznaczne z
const fieldsWithErrors = form.querySelectorAll(`[name="name"],[name="email"],[name="message"]...`)
Zastosujmy powyższą logikę w naszej funkcji i odpalmy ją w funkcji wysyłającej formularz:
function afterSubmit(res) {
if (res.errors) {
const selectors = res.errors.map(el => `[name="${el}"]`);
const fieldsWithErrors = form.querySelectorAll(selectors.join(","));
for (const field of fieldsWithErrors) {
field.classList.add("is-invalid");
}
} else {
if (res.status === "success") {
showSubmitSuccess();
}
if (res.status === "error") {
showSubmitError(res.status);
}
}
}
function submitForm() {
let formErrors = checkElements();
if (!formErrors) {
const formData = new FormData(form);
const submit = form.querySelector("[type=submit]");
submit.disabled = true;
submit.classList.add("loading");
try {
const response = await makeRequest(formData);
afterSubmit(response);
} catch(err) {
//tutaj błędy, ale to za chwilę
}
submit.disabled = false;
submit.classList.remove("loading");
}
}
Odpowiedź z serwera, błąd serwera
Gdy dostajemy odpowiedź, odpalamy funkcję afterSubmit(), która w zależności od zwróconego statusu odpala funkcje showSubmitError() (linia 10 w powyższym listingu) lub showSubmitSuccess() (linia 13).
Dla statusu "error", możemy dodać do formularza tekst z ogólnym komunikatem o błędzie:
function showSubmitError() {
formMessage.innerHTML = "Wysłanie wiadomości się nie powiodło";
}
Tą samą funkcję możemy dodatkowo użyć, by wyświetlić błędy dla samego fetch (np. gdy występuje brak połączenia, albo adres wysyłki jest błędny):
async function submitForm() {
let formErrors = checkElements();
if (!formErrors) {
const submit = form.querySelector("[type=submit]");
submit.disabled = true;
submit.classList.add("loading");
const formData = new FormData(form);
try {
const response = await makeRequest(formData);
afterSubmit(response);
} catch(err) {
showSubmitError();
}
submit.disabled = false;
submit.classList.remove("loading");
}
}
Odpowiedź z serwera, wreszcie pozytywnie
Zajmijmy się teraz pozytywną odpowiedzią. Możemy tutaj oczywiście wstawiać dynamicznie tekst tak samo, jak powyżej w przypadku błędów. Ja zazwyczaj po pozytywnej wysyłce zastępuję cały formularz nową zawartością. Dzięki temu będzie on trochę bardziej odporny na proste ataki:
function showSubmitSuccess() {
const div = document.createElement("div");
div.classList.add("form-send-success");
form.after(div);
div.innerHTML = `
<strong>Wiadomość została wysłana</strong>
<span>Dziękujemy za kontakt. Postaramy się odpowiedzieć jak najszybciej</span>
`;
form.remove();
}
Prosty antyspam
Problem z formularzami takimi jak powyższy jest taki, że bardzo lubią je roboty rozsyłające spam. Aby im przeszkodzić, musimy jakoś zabezpieczyć nasze dzieło. Jedną z łatwiejszych metod jest tak zwany honeypot. Metoda ta polega 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>
.honey-row {
display:none !important;
}
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ść. Jeżeli ma, przerywamy wysyłkę, 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"] = "success";
}
header("Content-Type: application/json");
echo json_encode($return);
}
Recaptcha
Powyższy sposób nie jest idealny i bardzo łatwo można go obejść. Głupi robot może i wypełni powyższe pole, ale użytkownik bandyta nie musi.
Mimo wszystko w realnych projektach warto skorzystać z Recaptchy. Wersja 3 jest niewidoczna dla użytkownika (nie trzeba klikać jakiś dziwnych obrazków), a i jest bardzo łatwa do zaimplementowania.
Po pierwsze musimy wejść na stronę https://developers.google.com/recaptcha. Następnie na górze strony wchodzimy w Recaptcha v3 admin console. Po wejściu w link musimy podać etykietę (nazwa porządkowa), wybrać wersję recaptchy (v3) oraz domenę (adres) naszej strony. Po przejściu dalej dostaniemy 2 klucze. Jeden do użycia w kodzie html/js strony, a drugi do użycia po stronie serwera (np. w kodzie php).
Wystarczy potem postępować zgodnie z punktami z tej odpowiedzi.
Przykładowa implementacja tego rozwiązania w naszym kodzie może mieć postać:
async function submitForm() {
let formErrors = checkElements();
if (!formErrors) {
//tutaj wysyłka
const formData = new FormData(form);
const submit = form.querySelector("[type=submit]");
submit.disabled = true;
submit.classList.add("loading");
grecaptcha.ready(() => {
grecaptcha.execute(recaptchaKey, {action: 'submit'}).then(async (token) => {
formData.append("token", token);
try {
const response = await makeRequest(formData);
afterSubmit(response);
} catch(err) {
showSubmitError(err);
}
submit.disabled = false;
submit.classList.remove("loading");
});
});
}
}
form.addEventListener("submit", submit);
Po stronie serwera:
<?php
$mailToSend = "twoj-email@lorem.pl";
//klucz pobierzesz ze strony gdzie zakładałeś recaptche
$secretRecaptchaKey = '...';
if ($_SERVER["REQUEST_METHOD"] === "POST") {
$name = $_POST["name"];
$email = $_POST["email"];
$message = $_POST["email"];
$antiSpam = $_POST["honey"];
$errors = Array();
$return = Array();
if (!isset($_POST['token'])) die;
$token = $_POST['token'];
$response = file_get_contents(
"https://www.google.com/recaptcha/api/siteverify?secret=$secretRecaptchaKey&response=$token&remoteip=".$_SERVER['REMOTE_ADDR']
);
$response = json_decode($response);
if ($response->success === false) {
header("Content-Type: application/json");
die('{"status": "error"}'); //lub {"status" : "success"} dla zmylenia
}
//tutaj dalsza część skryptu który powyżej był w fragmencie "po stronie serwera"...
$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"] = "success";
} else {
$return["status"] = "error";
}
}
header("Content-Type: application/json");
echo json_encode($return);
}
Demo
Gotowy formularz znajdziesz poniżej:
demo