3.6 טפסים וולידציה הרצאה
טפסים ב-JavaScript¶
טפסים הם חלק מרכזי בכל אפליקציית ווב. רישום, התחברות, חיפוש, הזמנה - כל אלה דורשים טפסים.
ב-HTML למדנו איך לבנות טפסים. עכשיו נלמד איך לשלוט בהם עם JavaScript - לגשת לערכים, לוודא שהנתונים תקינים, ולהגיב בזמן אמת לקלט המשתמש.
אירוע submit¶
כשלוחצים על כפתור submit בטופס (או לוחצים Enter בתוך input), הדפדפן מפעיל את אירוע ה-submit על אלמנט ה-form.
ברירת המחדל של הדפדפן היא לשלוח את הטופס לשרת ולטעון מחדש את הדף. אבל אנחנו רוצים לטפל בנתונים ב-JavaScript, אז נעצור את ההתנהגות הזו:
<form id="login-form">
<input type="text" name="username" placeholder="username">
<input type="password" name="password" placeholder="password">
<button type="submit">login</button>
</form>
let form = document.getElementById("login-form");
form.addEventListener("submit", function (event) {
event.preventDefault(); // prevent the page from reloading
// now we can handle the form data with JavaScript
console.log("form submitted!");
});
event.preventDefault() עוצר את ההתנהגות הרגילה של הדפדפן. בלי זה, הדף ייטען מחדש ולא נוכל לעשות כלום עם הנתונים.
חשוב לזכור¶
- האירוע submit מופעל על אלמנט ה-form, לא על הכפתור
- הוא מופעל גם כשלוחצים Enter בתוך input (לא רק בלחיצה על הכפתור)
- חובה לקרוא ל-
event.preventDefault()אם רוצים לטפל בנתונים ב-JS
גישה לערכי הטופס¶
יש כמה דרכים לגשת לערכים שהמשתמש הזין בטופס.
דרך 1 - input.value¶
הדרך הישירה ביותר - לגשת ל-input ולקרוא את ה-value שלו:
form.addEventListener("submit", function (event) {
event.preventDefault();
let username = document.querySelector('[name="username"]').value;
let password = document.querySelector('[name="password"]').value;
console.log("username:", username);
console.log("password:", password);
});
דרך 2 - form.elements¶
ל-form יש property בשם elements שמכיל את כל שדות הטופס. אפשר לגשת אליהם לפי name:
form.addEventListener("submit", function (event) {
event.preventDefault();
let username = form.elements.username.value;
let password = form.elements.password.value;
console.log("username:", username);
console.log("password:", password);
});
זו הדרך הנקייה ביותר - לא צריך querySelector, רק גישה לפי שם השדה.
סוגי שדות שונים¶
<form id="profile-form">
<input type="text" name="name" value="Dan">
<input type="email" name="email" value="dan@email.com">
<input type="number" name="age" value="25">
<input type="checkbox" name="newsletter" checked>
<select name="country">
<option value="il">Israel</option>
<option value="us">USA</option>
<option value="uk">UK</option>
</select>
<input type="radio" name="gender" value="male">
<input type="radio" name="gender" value="female">
<textarea name="bio">Hello world</textarea>
</form>
let form = document.getElementById("profile-form");
// text, email, number, textarea - use .value
let name = form.elements.name.value; // "Dan"
let email = form.elements.email.value; // "dan@email.com"
let age = form.elements.age.value; // "25" (string! not number)
let bio = form.elements.bio.value; // "Hello world"
// checkbox - use .checked (boolean)
let newsletter = form.elements.newsletter.checked; // true
// select - use .value
let country = form.elements.country.value; // "il"
// radio buttons - need to find the checked one
let gender = form.elements.gender.value; // value of the checked radio
שימו לב - input.value תמיד מחזיר string, גם אם ה-type הוא number. אם צריכים מספר, חובה להמיר:
FormData API¶
הדפדפן מספק API נוח מאוד לעבודה עם נתוני טפסים - FormData.
form.addEventListener("submit", function (event) {
event.preventDefault();
let formData = new FormData(form);
// get a single value
let username = formData.get("username");
let password = formData.get("password");
console.log(username, password);
});
מתודות שימושיות של FormData¶
let formData = new FormData(form);
// get a single value
formData.get("username"); // "Dan"
// get all values for a field (useful for multiple checkboxes)
formData.getAll("hobbies"); // ["reading", "coding", "gaming"]
// check if a field exists
formData.has("email"); // true
// iterate over all fields
for (let [key, value] of formData.entries()) {
console.log(key + ": " + value);
}
המרה לאובייקט רגיל¶
הטריק הכי שימושי - המרת FormData לאובייקט JavaScript רגיל:
form.addEventListener("submit", function (event) {
event.preventDefault();
let formData = new FormData(form);
let data = Object.fromEntries(formData);
console.log(data);
// { username: "Dan", password: "12345", country: "il" }
});
Object.fromEntries() לוקח את כל הזוגות key-value של FormData והופך אותם לאובייקט רגיל.
זה מאוד נוח כשרוצים לשלוח את הנתונים לשרת (בהמשך הקורס נלמד איך).
חשוב¶
Object.fromEntries() לוקח רק את הערך האחרון לכל key. אם יש כמה checkboxes עם אותו name, רק אחד יישמר. במקרה כזה, צריך טיפול מיוחד:
let data = Object.fromEntries(formData);
// override with getAll for multi-value fields
data.hobbies = formData.getAll("hobbies");
ולידציה ב-HTML5¶
לפני שכותבים ולידציה ב-JavaScript, כדאי להכיר את הולידציה המובנית ב-HTML. היא חינמית, אוטומטית, ומספקת חוויה טובה למשתמש.
תכונות ולידציה¶
<!-- required - the field must have a value -->
<input type="text" name="username" required>
<!-- minlength / maxlength - text length limits -->
<input type="text" name="username" minlength="3" maxlength="20">
<!-- min / max - number range -->
<input type="number" name="age" min="18" max="120">
<!-- pattern - regex validation -->
<input type="text" name="phone" pattern="[0-9]{10}" title="10 digits">
<!-- type validation - the browser validates automatically -->
<input type="email" name="email"> <!-- must be a valid email format -->
<input type="url" name="website"> <!-- must be a valid URL -->
<input type="number" name="count"> <!-- must be a number -->
כשהמשתמש מנסה לשלוח טופס עם שדות לא תקינים, הדפדפן מציג הודעת שגיאה אוטומטית ומונע את השליחה.
הצגת הודעות שגיאה מותאמות¶
let usernameInput = document.querySelector('[name="username"]');
usernameInput.addEventListener("invalid", function () {
if (usernameInput.validity.valueMissing) {
usernameInput.setCustomValidity("please enter a username");
} else if (usernameInput.validity.tooShort) {
usernameInput.setCustomValidity("username must be at least 3 characters");
}
});
// clear custom message when user starts typing
usernameInput.addEventListener("input", function () {
usernameInput.setCustomValidity("");
});
setCustomValidity() מחליף את הודעת השגיאה הרגילה של הדפדפן בהודעה שלנו. חשוב לאפס אותה (מחרוזת ריקה) כשהמשתמש מתחיל להקליד, אחרת השדה תמיד ייחשב כלא תקין.
ולידציה ב-JavaScript¶
לפעמים ולידציית HTML לא מספיקה ואנחנו צריכים בדיקות מותאמות. למשל - לוודא ששני שדות סיסמה זהים, או שתאריך הוא בטווח מסוים.
form.addEventListener("submit", function (event) {
event.preventDefault();
let username = form.elements.username.value.trim();
let email = form.elements.email.value.trim();
let password = form.elements.password.value;
let confirmPassword = form.elements.confirmPassword.value;
let errors = [];
// check username
if (username.length < 3) {
errors.push("username must be at least 3 characters");
}
// check email format (simple check)
if (!email.includes("@")) {
errors.push("please enter a valid email");
}
// check password strength
if (password.length < 8) {
errors.push("password must be at least 8 characters");
}
// check passwords match
if (password !== confirmPassword) {
errors.push("passwords do not match");
}
// show errors or submit
if (errors.length > 0) {
let errorDiv = document.getElementById("errors");
errorDiv.innerHTML = errors.map(function (err) {
return "<p>" + err + "</p>";
}).join("");
} else {
console.log("form is valid! sending data...");
}
});
הגישה הזו נותנת שליטה מלאה - אפשר לבדוק כל תנאי שרוצים ולהציג הודעות שגיאה מותאמות.
אירוע input מול אירוע change¶
שני אירועים חשובים לעבודה עם שדות טופס:
input - מופעל על כל שינוי¶
let nameInput = document.querySelector('[name="username"]');
nameInput.addEventListener("input", function () {
console.log("current value:", nameInput.value);
// fires on EVERY keystroke, paste, delete, etc.
});
האירוע input מופעל בכל פעם שהערך משתנה - כל הקשה, כל הדבקה, כל מחיקה.
change - מופעל כשהמשתמש מסיים¶
nameInput.addEventListener("change", function () {
console.log("final value:", nameInput.value);
// fires when the input loses focus (blur) AND the value has changed
});
האירוע change מופעל רק כשהמשתמש עוזב את השדה (blur) - ורק אם הערך באמת השתנה.
מתי להשתמש במה?¶
- input - ולידציה בזמן אמת, ספירת תווים, חיפוש מיידי, סינון רשימה תוך כדי הקלדה
- change - שמירת ערך סופי, שליחת בקשה לשרת, חישובים שלא צריכים לקרות על כל הקשה
דוגמה - ספירת תווים¶
let textarea = document.querySelector('[name="bio"]');
let charCount = document.getElementById("char-count");
textarea.addEventListener("input", function () {
charCount.textContent = textarea.value.length;
});
ולידציה בזמן אמת - Real-Time Validation¶
הדפוס הכי נפוץ - לוודא את השדות תוך כדי הקלדה ולהציג הודעות שגיאה מיד:
<form id="register-form">
<div class="field">
<label for="username">username</label>
<input type="text" id="username" name="username">
<span class="error-msg"></span>
</div>
<div class="field">
<label for="email">email</label>
<input type="email" id="email" name="email">
<span class="error-msg"></span>
</div>
<div class="field">
<label for="password">password</label>
<input type="password" id="password" name="password">
<span class="error-msg"></span>
</div>
<button type="submit">register</button>
</form>
.field {
margin-bottom: 15px;
}
.error-msg {
color: red;
font-size: 0.85rem;
display: block;
min-height: 20px;
}
input.invalid {
border: 2px solid red;
}
input.valid {
border: 2px solid green;
}
let form = document.getElementById("register-form");
// validate a single field
function validateField(input) {
let errorSpan = input.parentElement.querySelector(".error-msg");
let value = input.value.trim();
let error = "";
switch (input.name) {
case "username":
if (value.length === 0) {
error = "username is required";
} else if (value.length < 3) {
error = "username must be at least 3 characters";
}
break;
case "email":
if (value.length === 0) {
error = "email is required";
} else if (!value.includes("@") || !value.includes(".")) {
error = "please enter a valid email";
}
break;
case "password":
if (value.length === 0) {
error = "password is required";
} else if (value.length < 8) {
error = "password must be at least 8 characters";
}
break;
}
// update UI
errorSpan.textContent = error;
if (error) {
input.classList.add("invalid");
input.classList.remove("valid");
} else {
input.classList.add("valid");
input.classList.remove("invalid");
}
return error === "";
}
// validate on input (real-time)
form.querySelectorAll("input").forEach(function (input) {
input.addEventListener("input", function () {
validateField(input);
});
});
// validate all on submit
form.addEventListener("submit", function (event) {
event.preventDefault();
let allValid = true;
form.querySelectorAll("input").forEach(function (input) {
if (!validateField(input)) {
allValid = false;
}
});
if (allValid) {
let data = Object.fromEntries(new FormData(form));
console.log("form is valid:", data);
}
});
כל שדה מקבל מסגרת ירוקה כשהוא תקין ואדומה כשלא, עם הודעת שגיאה ספציפית.
דיבאונס - Debounce¶
כשמריצים ולידציה על כל הקשה, זה יכול להיות מעצבן למשתמש. הוא רק התחיל להקליד ואנחנו כבר מראים שגיאה.
הפתרון הוא debounce - לחכות שהמשתמש יפסיק להקליד לפני שמבצעים את הבדיקה:
function debounce(func, delay) {
let timeoutId;
return function () {
clearTimeout(timeoutId);
timeoutId = setTimeout(func, delay);
};
}
// validate only after user stops typing for 500ms
let usernameInput = document.querySelector('[name="username"]');
usernameInput.addEventListener("input", debounce(function () {
validateField(usernameInput);
}, 500));
debounce יוצר פונקציה חדשה שמחכה delay מילישניות אחרי הקריאה האחרונה. אם המשתמש ממשיך להקליד, הטיימר מתאפס. רק כשהוא מפסיק, הפונקציה מופעלת.
זה שימושי במיוחד כשהולידציה כוללת פנייה לשרת (למשל בדיקה אם שם משתמש תפוס) - לא רוצים לשלוח בקשה על כל הקשה.
סריאליזציה - Serialization¶
סריאליזציה זה המונח להמרת נתוני הטופס למבנה שאפשר לשלוח או לשמור.
המרה לאובייקט¶
function formToObject(form) {
let formData = new FormData(form);
let data = {};
for (let [key, value] of formData.entries()) {
// handle multiple values (checkboxes)
if (data[key]) {
if (!Array.isArray(data[key])) {
data[key] = [data[key]];
}
data[key].push(value);
} else {
data[key] = value;
}
}
return data;
}
form.addEventListener("submit", function (event) {
event.preventDefault();
let data = formToObject(form);
console.log(data);
// { username: "Dan", hobbies: ["reading", "coding"], country: "il" }
});
הפונקציה הזו יודעת לטפל גם בשדות שיש להם כמה ערכים (כמו checkboxes).
המרה ל-JSON¶
let jsonString = JSON.stringify(formToObject(form));
console.log(jsonString);
// '{"username":"Dan","hobbies":["reading","coding"],"country":"il"}'
זה מה שנשלח לשרת ב-API requests (נלמד בהמשך הקורס).
דוגמה מלאה - טופס הרשמה עם ולידציה¶
נבנה טופס הרשמה שלם עם ולידציה בזמן אמת:
<form id="signup-form">
<div class="field">
<label for="fullname">full name</label>
<input type="text" id="fullname" name="fullname">
<span class="error-msg"></span>
</div>
<div class="field">
<label for="email">email</label>
<input type="email" id="email" name="email">
<span class="error-msg"></span>
</div>
<div class="field">
<label for="password">password</label>
<input type="password" id="password" name="password">
<span class="error-msg"></span>
<div id="password-strength"></div>
</div>
<div class="field">
<label for="confirm">confirm password</label>
<input type="password" id="confirm" name="confirm">
<span class="error-msg"></span>
</div>
<div class="field">
<label for="age">age</label>
<input type="number" id="age" name="age">
<span class="error-msg"></span>
</div>
<div class="field">
<label>
<input type="checkbox" name="terms">
I agree to the terms
</label>
<span class="error-msg"></span>
</div>
<button type="submit">sign up</button>
</form>
<div id="success-msg" style="display: none; color: green;"></div>
let form = document.getElementById("signup-form");
// validation rules for each field
let rules = {
fullname: function (value) {
if (!value) return "name is required";
if (value.length < 2) return "name must be at least 2 characters";
return "";
},
email: function (value) {
if (!value) return "email is required";
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) return "invalid email format";
return "";
},
password: function (value) {
if (!value) return "password is required";
if (value.length < 8) return "password must be at least 8 characters";
if (!/[A-Z]/.test(value)) return "password must contain an uppercase letter";
if (!/[0-9]/.test(value)) return "password must contain a number";
return "";
},
confirm: function (value) {
let password = form.elements.password.value;
if (!value) return "please confirm your password";
if (value !== password) return "passwords do not match";
return "";
},
age: function (value) {
if (!value) return "age is required";
let num = Number(value);
if (num < 16 || num > 120) return "age must be between 16 and 120";
return "";
},
terms: function (value, input) {
if (!input.checked) return "you must agree to the terms";
return "";
}
};
// validate a single field
function validateField(input) {
let name = input.name;
if (!rules[name]) return true;
let value = input.type === "checkbox" ? "" : input.value.trim();
let error = rules[name](value, input);
let errorSpan = input.closest(".field").querySelector(".error-msg");
errorSpan.textContent = error;
if (error) {
input.classList.add("invalid");
input.classList.remove("valid");
return false;
} else {
input.classList.add("valid");
input.classList.remove("invalid");
return true;
}
}
// password strength indicator
function updatePasswordStrength(value) {
let indicator = document.getElementById("password-strength");
let strength = 0;
if (value.length >= 8) strength++;
if (/[A-Z]/.test(value)) strength++;
if (/[0-9]/.test(value)) strength++;
if (/[^A-Za-z0-9]/.test(value)) strength++;
let labels = ["", "weak", "fair", "good", "strong"];
let colors = ["", "#e74c3c", "#f39c12", "#3498db", "#2ecc71"];
indicator.textContent = labels[strength];
indicator.style.color = colors[strength];
}
// real-time validation on all inputs
form.querySelectorAll("input").forEach(function (input) {
let eventType = (input.type === "checkbox") ? "change" : "input";
input.addEventListener(eventType, function () {
validateField(input);
if (input.name === "password") {
updatePasswordStrength(input.value);
// also re-validate confirm if it has a value
let confirm = form.elements.confirm;
if (confirm.value) validateField(confirm);
}
});
});
// submit
form.addEventListener("submit", function (event) {
event.preventDefault();
let allValid = true;
form.querySelectorAll("input").forEach(function (input) {
if (!validateField(input)) {
allValid = false;
}
});
if (allValid) {
let data = Object.fromEntries(new FormData(form));
delete data.confirm; // don't need confirm password in the data
delete data.terms; // don't need the checkbox value
console.log("registration data:", data);
let successMsg = document.getElementById("success-msg");
successMsg.textContent = "registration successful! welcome, " + data.fullname;
successMsg.style.display = "block";
form.reset();
form.querySelectorAll("input").forEach(function (input) {
input.classList.remove("valid", "invalid");
});
}
});
הדוגמה הזו מדגימה:
- ולידציה בזמן אמת על כל שדה
- כללי ולידציה מותאמים לכל שדה
- מד חוזק סיסמה
- בדיקת התאמה בין סיסמאות (כשמשנים את הסיסמה, גם שדה האישור נבדק מחדש)
- איסוף נתונים עם FormData והמרה לאובייקט
- הודעת הצלחה והתאפסות הטופס
טופס רב-שלבי - Multi-Step Form¶
קונספט מתקדם - טופס שמחולק לכמה שלבים. המשתמש ממלא חלק, לוחץ "הבא", ומגיע לחלק הבא.
העיקרון הוא פשוט: כל שלב הוא div שמוצג/מוסתר:
<form id="multi-step-form">
<div class="step active" data-step="1">
<h3>step 1: personal info</h3>
<input type="text" name="name" placeholder="name">
<input type="email" name="email" placeholder="email">
<button type="button" class="next-btn">next</button>
</div>
<div class="step" data-step="2">
<h3>step 2: address</h3>
<input type="text" name="city" placeholder="city">
<input type="text" name="street" placeholder="street">
<button type="button" class="prev-btn">back</button>
<button type="button" class="next-btn">next</button>
</div>
<div class="step" data-step="3">
<h3>step 3: confirm</h3>
<div id="summary"></div>
<button type="button" class="prev-btn">back</button>
<button type="submit">submit</button>
</div>
</form>
let multiForm = document.getElementById("multi-step-form");
let currentStep = 1;
let totalSteps = 3;
function showStep(step) {
multiForm.querySelectorAll(".step").forEach(function (s) {
s.classList.remove("active");
});
multiForm.querySelector('[data-step="' + step + '"]').classList.add("active");
currentStep = step;
}
// next buttons
multiForm.querySelectorAll(".next-btn").forEach(function (btn) {
btn.addEventListener("click", function () {
// validate current step before moving on
let currentStepDiv = multiForm.querySelector('[data-step="' + currentStep + '"]');
let inputs = currentStepDiv.querySelectorAll("input");
let valid = true;
inputs.forEach(function (input) {
if (!input.value.trim()) {
input.style.borderColor = "red";
valid = false;
} else {
input.style.borderColor = "";
}
});
if (valid && currentStep < totalSteps) {
// if going to summary step, show the data
if (currentStep + 1 === totalSteps) {
let data = Object.fromEntries(new FormData(multiForm));
let summary = document.getElementById("summary");
summary.innerHTML = Object.keys(data).map(function (key) {
return "<p><strong>" + key + ":</strong> " + data[key] + "</p>";
}).join("");
}
showStep(currentStep + 1);
}
});
});
// back buttons
multiForm.querySelectorAll(".prev-btn").forEach(function (btn) {
btn.addEventListener("click", function () {
if (currentStep > 1) {
showStep(currentStep - 1);
}
});
});
// final submit
multiForm.addEventListener("submit", function (event) {
event.preventDefault();
let data = Object.fromEntries(new FormData(multiForm));
console.log("final data:", data);
});
הרעיון המרכזי: כל ה-inputs נמצאים בתוך form אחד, רק ה-div שמכיל כל שלב מוצג/מוסתר. כשעושים new FormData(form) בסוף, כל הנתונים מכל השלבים נאספים.
סיכום¶
- אירוע submit מופעל על ה-form - חובה
event.preventDefault()כדי לטפל ב-JS - גישה לערכים:
input.value,form.elements.name.value,checkbox.checked FormData- API נוח לאיסוף נתונים;Object.fromEntries(new FormData(form))ממיר לאובייקט- ולידציית HTML5:
required,pattern,min/max,minlength/maxlength,type setCustomValidity()- הודעות שגיאה מותאמות- אירוע
inputמופעל על כל שינוי; אירועchangeמופעל כשעוזבים את השדה - ולידציה בזמן אמת - בודקים על כל הקשה ומציגים שגיאות מיד
- דיבאונס - מחכים שהמשתמש יפסיק להקליד לפני שמפעילים פונקציה
- סריאליזציה - המרת נתוני טופס לאובייקט או JSON