לדלג לתוכן

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

תרגיל 4 - טופס הזמנה עם FormData

<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, אחרת השדה תמיד ייחשב לא תקין
  • הדפדפן מציג את ההודעות המותאמות בבועת השגיאה הרגילה שלו