3.6 טפסים וולידציה פתרון
פתרון תרגול טפסים וולידציה
תרגיל 1 - טופס התחברות בסיסי
<form id="login-form">
<input type="text" name="username" placeholder="username">
<input type="password" name="password" placeholder="password">
<button type="submit">login</button>
</form>
<div id="message"></div>
let form = document.getElementById("login-form");
let message = document.getElementById("message");
form.addEventListener("submit", function (event) {
event.preventDefault();
let username = form.elements.username.value.trim();
let password = form.elements.password.value;
console.log({ username: username, password: password });
if (!username || !password) {
message.textContent = "all fields are required";
message.style.color = "red";
return;
}
if (username === "admin" && password === "1234") {
message.textContent = "login successful";
message.style.color = "green";
} else {
message.textContent = "invalid username or password";
message.style.color = "red";
}
});
event.preventDefault() מונע טעינה מחדש
- בודקים ששני השדות לא ריקים
- בודקים התאמה לערכים הקבועים
תרגיל 2 - ספירת תווים ומגבלות
<div id="tweet-app">
<textarea id="tweet-input" maxlength="280" placeholder="what's happening?"></textarea>
<div>
<span id="char-counter" style="color: green;">0/280 characters</span>
</div>
<button id="post-btn" disabled>post</button>
<ul id="posts-list"></ul>
</div>
let textarea = document.getElementById("tweet-input");
let counter = document.getElementById("char-counter");
let postBtn = document.getElementById("post-btn");
let postsList = document.getElementById("posts-list");
textarea.addEventListener("input", function () {
let length = textarea.value.length;
let remaining = 280 - length;
counter.textContent = length + "/280 characters";
// color based on remaining characters
if (remaining < 10) {
counter.style.color = "red";
} else if (remaining < 20) {
counter.style.color = "orange";
} else {
counter.style.color = "green";
}
// enable/disable post button
postBtn.disabled = length === 0;
});
postBtn.addEventListener("click", function () {
let text = textarea.value.trim();
if (!text) return;
let li = document.createElement("li");
li.textContent = text;
postsList.prepend(li); // add to top of list
textarea.value = "";
counter.textContent = "0/280 characters";
counter.style.color = "green";
postBtn.disabled = true;
});
- אירוע
input מתעדכן על כל הקשה
maxlength="280" מונע הקלדה מעבר למגבלה
- הכפתור מושבת כשהשדה ריק
תרגיל 3 - טופס הרשמה עם ולידציה בזמן אמת
<form id="register-form">
<div class="field">
<label>full name</label>
<input type="text" name="fullname">
<span class="error"></span>
</div>
<div class="field">
<label>email</label>
<input type="text" name="email">
<span class="error"></span>
</div>
<div class="field">
<label>password</label>
<input type="password" name="password">
<span class="error"></span>
<div id="strength"></div>
</div>
<div class="field">
<label>confirm password</label>
<input type="password" name="confirm">
<span class="error"></span>
</div>
<div class="field">
<label>age</label>
<input type="number" name="age">
<span class="error"></span>
</div>
<button type="submit" id="submit-btn" disabled>register</button>
</form>
<pre id="result"></pre>
input.valid { border: 2px solid green; }
input.invalid { border: 2px solid red; }
.error { color: red; font-size: 0.85rem; }
let form = document.getElementById("register-form");
let submitBtn = document.getElementById("submit-btn");
let validators = {
fullname: function (val) {
if (!val) return "name is required";
if (val.length < 2) return "name must be at least 2 characters";
return "";
},
email: function (val) {
if (!val) return "email is required";
if (!val.includes("@") || !val.includes(".")) return "invalid email";
return "";
},
password: function (val) {
if (!val) return "password is required";
if (val.length < 8) return "at least 8 characters";
if (!/[A-Z]/.test(val)) return "must contain an uppercase letter";
if (!/[0-9]/.test(val)) return "must contain a number";
return "";
},
confirm: function (val) {
if (!val) return "please confirm password";
if (val !== form.elements.password.value) return "passwords do not match";
return "";
},
age: function (val) {
if (!val) return "age is required";
let n = Number(val);
if (n < 16 || n > 120) return "age must be between 16 and 120";
return "";
}
};
function validate(input) {
let fn = validators[input.name];
if (!fn) return true;
let error = fn(input.value.trim());
let span = input.closest(".field").querySelector(".error");
span.textContent = error;
input.classList.toggle("invalid", error !== "");
input.classList.toggle("valid", error === "");
return error === "";
}
function updatePasswordStrength(val) {
let s = 0;
if (val.length >= 8) s++;
if (/[A-Z]/.test(val)) s++;
if (/[0-9]/.test(val)) s++;
if (/[^A-Za-z0-9]/.test(val)) s++;
let labels = ["", "weak", "fair", "good", "strong"];
let colors = ["", "red", "orange", "#3498db", "green"];
let el = document.getElementById("strength");
el.textContent = labels[s];
el.style.color = colors[s];
}
function checkAllValid() {
let allValid = true;
form.querySelectorAll("input").forEach(function (input) {
if (validators[input.name]) {
let error = validators[input.name](input.value.trim());
if (error) allValid = false;
}
});
submitBtn.disabled = !allValid;
}
form.querySelectorAll("input").forEach(function (input) {
input.addEventListener("input", function () {
validate(input);
if (input.name === "password") {
updatePasswordStrength(input.value);
if (form.elements.confirm.value) validate(form.elements.confirm);
}
checkAllValid();
});
});
form.addEventListener("submit", function (event) {
event.preventDefault();
let data = Object.fromEntries(new FormData(form));
delete data.confirm;
document.getElementById("result").textContent = JSON.stringify(data, null, 2);
form.reset();
form.querySelectorAll("input").forEach(function (input) {
input.classList.remove("valid", "invalid");
});
submitBtn.disabled = true;
});
- כל שדה מקבל ולידציה בזמן אמת על אירוע
input
- מד חוזק סיסמה מחשב ניקוד לפי מורכבות
- כפתור ה-submit מושבת עד שכל הולידציות עוברות
classList.toggle עם תנאי הוא דרך נוחה להוסיף/להסיר class
<form id="pizza-form">
<input type="text" name="name" placeholder="name" required>
<input type="tel" name="phone" placeholder="phone" required>
<select name="size">
<option value="small">small (40)</option>
<option value="medium">medium (60)</option>
<option value="large">large (80)</option>
</select>
<fieldset>
<legend>toppings (+5 each)</legend>
<label><input type="checkbox" name="toppings" value="mushrooms"> mushrooms</label>
<label><input type="checkbox" name="toppings" value="olives"> olives</label>
<label><input type="checkbox" name="toppings" value="onions"> onions</label>
<label><input type="checkbox" name="toppings" value="peppers"> peppers</label>
<label><input type="checkbox" name="toppings" value="extra cheese"> extra cheese</label>
</fieldset>
<textarea name="notes" placeholder="notes"></textarea>
<fieldset>
<legend>delivery method</legend>
<label><input type="radio" name="delivery" value="delivery" checked> delivery</label>
<label><input type="radio" name="delivery" value="pickup"> pickup</label>
</fieldset>
<button type="submit">order</button>
</form>
<div id="order-summary"></div>
let form = document.getElementById("pizza-form");
let prices = { small: 40, medium: 60, large: 80 };
let toppingPrice = 5;
form.addEventListener("submit", function (event) {
event.preventDefault();
let formData = new FormData(form);
let name = formData.get("name");
let phone = formData.get("phone");
let size = formData.get("size");
let toppings = formData.getAll("toppings");
let notes = formData.get("notes");
let delivery = formData.get("delivery");
// calculate price
let basePrice = prices[size];
let toppingsTotal = toppings.length * toppingPrice;
let totalPrice = basePrice + toppingsTotal;
// build summary
let summaryHTML =
"<h3>order summary</h3>" +
"<p><strong>name:</strong> " + name + "</p>" +
"<p><strong>phone:</strong> " + phone + "</p>" +
"<p><strong>size:</strong> " + size + " (" + basePrice + ")</p>" +
"<p><strong>toppings:</strong> " +
(toppings.length > 0 ? toppings.join(", ") + " (+" + toppingsTotal + ")" : "none") +
"</p>" +
"<p><strong>delivery:</strong> " + delivery + "</p>" +
(notes ? "<p><strong>notes:</strong> " + notes + "</p>" : "") +
"<p><strong>total: " + totalPrice + "</strong></p>";
document.getElementById("order-summary").innerHTML = summaryHTML;
});
FormData.get() מחזיר ערך בודד
FormData.getAll() מחזיר מערך של כל הערכים (חשוב ל-checkboxes)
- חישוב המחיר מבוסס על גודל + מספר תוספות
תרגיל 5 - חיפוש ברשימה עם debounce
<div>
<input type="text" id="search-input" placeholder="search countries...">
<p id="results-count"></p>
<ul id="countries-list"></ul>
</div>
let countries = [
"Argentina", "Australia", "Brazil", "Canada", "China",
"Denmark", "Egypt", "France", "Germany", "India",
"Israel", "Japan", "Mexico", "Netherlands", "Norway",
"Poland", "Russia", "Spain", "Sweden", "United States"
];
let list = document.getElementById("countries-list");
let searchInput = document.getElementById("search-input");
let resultsCount = document.getElementById("results-count");
// render all countries initially
function renderCountries(filtered) {
list.innerHTML = "";
if (filtered.length === 0) {
list.innerHTML = "<li>no results found</li>";
} else {
filtered.forEach(function (country) {
let li = document.createElement("li");
li.textContent = country;
list.appendChild(li);
});
}
resultsCount.textContent = "showing " + filtered.length + " of " + countries.length + " results";
}
// debounce function
function debounce(func, delay) {
let timeoutId;
return function () {
clearTimeout(timeoutId);
let args = arguments;
let context = this;
timeoutId = setTimeout(function () {
func.apply(context, args);
}, delay);
};
}
// filter function
function filterCountries() {
let query = searchInput.value.trim().toLowerCase();
let filtered = countries.filter(function (country) {
return country.toLowerCase().includes(query);
});
renderCountries(filtered);
}
// debounced search - 300ms
searchInput.addEventListener("input", debounce(filterCountries, 300));
// initial render
renderCountries(countries);
debounce מחכה 300ms אחרי הקשה אחרונה לפני שמסנן
toLowerCase() הופך חיפוש ל-case insensitive
includes() בודק אם המחרוזת מכילה את מחרוזת החיפוש
תרגיל 6 - טופס עריכת פרופיל עם שמירה
<form id="profile-form">
<label>name: <input type="text" name="name"></label>
<label>email: <input type="email" name="email"></label>
<label>bio: <textarea name="bio"></textarea></label>
<label>theme:
<select name="theme">
<option value="dark">dark</option>
<option value="light">light</option>
</select>
</label>
<label><input type="checkbox" name="notifications"> notifications</label>
<button type="submit">save</button>
<button type="button" id="reset-btn">reset</button>
</form>
<div id="save-msg" style="color: green; display: none;">changes saved!</div>
let profile = {
name: "Dan Cohen",
email: "dan@email.com",
bio: "Web developer",
theme: "dark",
notifications: true
};
// keep a copy of the original for reset
let originalProfile = Object.assign({}, profile);
let form = document.getElementById("profile-form");
// fill form with profile data
function fillForm() {
form.elements.name.value = profile.name;
form.elements.email.value = profile.email;
form.elements.bio.value = profile.bio;
form.elements.theme.value = profile.theme;
form.elements.notifications.checked = profile.notifications;
applyTheme(profile.theme);
}
// apply theme preview
function applyTheme(theme) {
document.body.style.background = theme === "dark" ? "#1a1a2e" : "#ffffff";
document.body.style.color = theme === "dark" ? "#ffffff" : "#000000";
}
// theme preview on change
form.elements.theme.addEventListener("change", function () {
applyTheme(this.value);
});
// save
form.addEventListener("submit", function (event) {
event.preventDefault();
let name = form.elements.name.value.trim();
let email = form.elements.email.value.trim();
// validation
if (!name) {
alert("name is required");
return;
}
if (!email || !email.includes("@")) {
alert("valid email is required");
return;
}
// update profile
profile.name = name;
profile.email = email;
profile.bio = form.elements.bio.value.trim();
profile.theme = form.elements.theme.value;
profile.notifications = form.elements.notifications.checked;
// save as new "original" for future resets
originalProfile = Object.assign({}, profile);
// show success message
let msg = document.getElementById("save-msg");
msg.style.display = "block";
setTimeout(function () {
msg.style.display = "none";
}, 3000);
});
// reset
document.getElementById("reset-btn").addEventListener("click", function () {
profile = Object.assign({}, originalProfile);
fillForm();
});
// initial fill
fillForm();
Object.assign({}, profile) יוצר עותק של האובייקט (לא הפניה)
- בשמירה מעדכנים גם את ה-originalProfile כך שהאיפוס יחזיר למצב שנשמר
setTimeout מסתיר את הודעת ההצלחה אחרי 3 שניות
- שינוי theme מיד מחיל תצוגה מקדימה
תרגיל 7 - טופס רב-שלבי
<div id="progress-bar">
<div class="progress-fill" style="width: 33%;"></div>
</div>
<p id="step-indicator">step 1 of 3</p>
<form id="multi-form">
<div class="step active" data-step="1">
<h3>personal info</h3>
<label>full name: <input type="text" name="fullname" required></label>
<label>email: <input type="email" name="email" required></label>
<label>phone: <input type="tel" name="phone"></label>
<button type="button" class="next-btn">next</button>
</div>
<div class="step" data-step="2">
<h3>address</h3>
<label>city: <input type="text" name="city" required></label>
<label>street: <input type="text" name="street" required></label>
<label>zip code: <input type="text" name="zip" pattern="[0-9]{7}"></label>
<button type="button" class="prev-btn">back</button>
<button type="button" class="next-btn">next</button>
</div>
<div class="step" data-step="3">
<h3>summary</h3>
<div id="summary"></div>
<label><input type="checkbox" name="terms" required> I agree to the terms</label>
<button type="button" class="prev-btn">back</button>
<button type="submit">submit</button>
</div>
</form>
#progress-bar {
width: 100%;
height: 8px;
background: #ddd;
border-radius: 4px;
margin-bottom: 10px;
}
.progress-fill {
height: 100%;
background: #3498db;
border-radius: 4px;
transition: width 0.3s ease;
}
.step {
display: none;
opacity: 0;
transition: opacity 0.3s ease;
}
.step.active {
display: block;
opacity: 1;
}
let form = document.getElementById("multi-form");
let progressFill = document.querySelector(".progress-fill");
let stepIndicator = document.getElementById("step-indicator");
let currentStep = 1;
let totalSteps = 3;
function showStep(step) {
// hide current step
let currentDiv = form.querySelector(".step.active");
currentDiv.classList.remove("active");
// show new step
setTimeout(function () {
let newDiv = form.querySelector('[data-step="' + step + '"]');
newDiv.classList.add("active");
}, 50);
currentStep = step;
progressFill.style.width = ((step / totalSteps) * 100) + "%";
stepIndicator.textContent = "step " + step + " of " + totalSteps;
}
function validateCurrentStep() {
let stepDiv = form.querySelector('[data-step="' + currentStep + '"]');
let inputs = stepDiv.querySelectorAll("input[required]");
let valid = true;
inputs.forEach(function (input) {
if (!input.value.trim()) {
input.style.borderColor = "red";
valid = false;
} else {
input.style.borderColor = "";
}
});
return valid;
}
function buildSummary() {
let formData = new FormData(form);
let summary = document.getElementById("summary");
let html = "";
for (let [key, value] of formData.entries()) {
if (key !== "terms" && value) {
html += "<p><strong>" + key + ":</strong> " + value + "</p>";
}
}
summary.innerHTML = html;
}
// next buttons
form.addEventListener("click", function (event) {
if (event.target.closest(".next-btn")) {
if (validateCurrentStep() && currentStep < totalSteps) {
if (currentStep + 1 === totalSteps) {
buildSummary();
}
showStep(currentStep + 1);
}
}
if (event.target.closest(".prev-btn")) {
if (currentStep > 1) {
showStep(currentStep - 1);
}
}
});
form.addEventListener("submit", function (event) {
event.preventDefault();
let termsChecked = form.elements.terms.checked;
if (!termsChecked) {
alert("you must agree to the terms");
return;
}
let data = Object.fromEntries(new FormData(form));
delete data.terms;
console.log(JSON.stringify(data, null, 2));
});
- כל שלב הוא div עם
display: none/block
transition: opacity יוצר אנימציית מעבר חלקה
- ולידציה מופעלת לפני מעבר לשלב הבא
- בשלב הסיכום,
FormData אוסף נתונים מכל השלבים
תרגיל 8 - ולידציה מותאמת עם setCustomValidity
<form id="custom-validation-form">
<label>
username (letters and numbers, 3-15 chars):
<input type="text" name="username" pattern="[A-Za-z0-9]{3,15}" required>
</label>
<label>
email:
<input type="email" name="email" required>
</label>
<label>
website:
<input type="url" name="website">
</label>
<label>
age:
<input type="number" name="age" min="18" max="99" required>
</label>
<button type="submit">send</button>
</form>
let form = document.getElementById("custom-validation-form");
// custom messages for each field
let messages = {
username: {
valueMissing: "please enter a username",
patternMismatch: "username must contain only letters and numbers, 3-15 characters"
},
email: {
valueMissing: "please enter an email address",
typeMismatch: "please enter a valid email address"
},
website: {
typeMismatch: "please enter a valid URL (starting with http:// or https://)"
},
age: {
valueMissing: "please enter your age",
rangeUnderflow: "you must be at least 18 years old",
rangeOverflow: "age cannot be more than 99"
}
};
// set custom validation messages on invalid event
form.querySelectorAll("input").forEach(function (input) {
input.addEventListener("invalid", function () {
let fieldMessages = messages[input.name];
if (!fieldMessages) return;
let validity = input.validity;
if (validity.valueMissing && fieldMessages.valueMissing) {
input.setCustomValidity(fieldMessages.valueMissing);
} else if (validity.typeMismatch && fieldMessages.typeMismatch) {
input.setCustomValidity(fieldMessages.typeMismatch);
} else if (validity.patternMismatch && fieldMessages.patternMismatch) {
input.setCustomValidity(fieldMessages.patternMismatch);
} else if (validity.rangeUnderflow && fieldMessages.rangeUnderflow) {
input.setCustomValidity(fieldMessages.rangeUnderflow);
} else if (validity.rangeOverflow && fieldMessages.rangeOverflow) {
input.setCustomValidity(fieldMessages.rangeOverflow);
}
});
// clear custom validity when user types
input.addEventListener("input", function () {
input.setCustomValidity("");
});
});
form.addEventListener("submit", function (event) {
// the browser will show custom messages if validation fails
// if validation passes, preventDefault and handle in JS
event.preventDefault();
let data = Object.fromEntries(new FormData(form));
console.log("valid data:", data);
});
setCustomValidity() מחליף את הודעת הדפדפן בהודעה מותאמת
validity object מכיל booleans שמתארים את סוג השגיאה
- חשוב לנקות את ה-customValidity על אירוע
input, אחרת השדה תמיד ייחשב לא תקין
- הדפדפן מציג את ההודעות המותאמות בבועת השגיאה הרגילה שלו