לדלג לתוכן

3.7 אחסון מקומי פתרון

פתרון תרגול אחסון מקומי


תרגיל 1 - שמירת שם

<h1 id="greeting"></h1>
<input type="text" id="name-input" placeholder="enter your name">
<button id="save-btn">save</button>
<button id="forget-btn">forget me</button>
let greeting = document.getElementById("greeting");
let nameInput = document.getElementById("name-input");

function updateGreeting() {
  let name = localStorage.getItem("savedName");
  if (name) {
    greeting.textContent = "welcome back, " + name + "!";
  } else {
    greeting.textContent = "welcome, stranger!";
  }
}

document.getElementById("save-btn").addEventListener("click", function () {
  let name = nameInput.value.trim();
  if (name) {
    localStorage.setItem("savedName", name);
    nameInput.value = "";
    updateGreeting();
  }
});

document.getElementById("forget-btn").addEventListener("click", function () {
  localStorage.removeItem("savedName");
  updateGreeting();
});

// on page load
updateGreeting();
  • getItem מחזיר null אם אין ערך, מה שגורם לתנאי if (name) לעבוד נכון
  • removeItem מוחק את ה-key הספציפי

תרגיל 2 - מונה לחיצות

<h1 id="count-display">0</h1>
<button id="plus-btn">+</button>
<button id="minus-btn">-</button>
<button id="reset-btn">reset</button>
let display = document.getElementById("count-display");

// load saved count or start at 0
let count = parseInt(localStorage.getItem("counter")) || 0;
display.textContent = count;

function updateCount(newCount) {
  count = newCount;
  display.textContent = count;
  localStorage.setItem("counter", count);
}

document.getElementById("plus-btn").addEventListener("click", function () {
  updateCount(count + 1);
});

document.getElementById("minus-btn").addEventListener("click", function () {
  updateCount(count - 1);
});

document.getElementById("reset-btn").addEventListener("click", function () {
  updateCount(0);
});
  • parseInt ממיר את המחרוזת מ-localStorage למספר
  • || 0 מטפל במקרה שאין ערך שמור (null -> NaN -> 0)
  • כל שינוי שומר מיד ל-localStorage

תרגיל 3 - בורר ערכת צבעים

<div id="theme-buttons">
  <button data-theme="light">light</button>
  <button data-theme="dark">dark</button>
  <button data-theme="blue">blue</button>
</div>
<h1>Theme Switcher</h1>
<p>Choose your preferred theme. It will be saved for next time.</p>
body.light { background: #ffffff; color: #000000; }
body.dark { background: #1a1a2e; color: #e0e0e0; }
body.blue { background: #0a1628; color: #e0e0e0; }

button.active {
  outline: 3px solid #3498db;
  font-weight: bold;
}
let themeButtons = document.getElementById("theme-buttons");

function applyTheme(theme) {
  // remove all theme classes
  document.body.className = theme;

  // update active button
  themeButtons.querySelectorAll("button").forEach(function (btn) {
    btn.classList.toggle("active", btn.dataset.theme === theme);
  });

  // save to localStorage
  localStorage.setItem("selectedTheme", theme);
}

// delegated handler for theme buttons
themeButtons.addEventListener("click", function (event) {
  let button = event.target.closest("button");
  if (!button) return;
  applyTheme(button.dataset.theme);
});

// load saved theme or default to light
let savedTheme = localStorage.getItem("selectedTheme") || "light";
applyTheme(savedTheme);
  • document.body.className = theme מחליף את כל ה-classes ב-class החדש
  • classList.toggle עם תנאי מדגיש רק את הכפתור הנכון
  • בטעינת הדף, נטען ה-theme השמור

תרגיל 4 - רשימת מועדפים

<p><span id="fav-count">0</span> favorites</p>
<button id="clear-favs">clear all favorites</button>
<div id="movies-grid"></div>
let movies = [
  "The Shawshank Redemption", "The Dark Knight", "Inception",
  "Pulp Fiction", "Fight Club", "The Matrix",
  "Interstellar", "Goodfellas", "The Godfather", "Parasite"
];

let moviesGrid = document.getElementById("movies-grid");
let favCountEl = document.getElementById("fav-count");

// load favorites from localStorage
function getFavorites() {
  return JSON.parse(localStorage.getItem("favorites")) || [];
}

function saveFavorites(favs) {
  localStorage.setItem("favorites", JSON.stringify(favs));
  favCountEl.textContent = favs.length;
}

function renderMovies() {
  let favorites = getFavorites();
  moviesGrid.innerHTML = "";

  movies.forEach(function (movie) {
    let isFav = favorites.includes(movie);
    let card = document.createElement("div");
    card.className = "movie-card";
    card.innerHTML =
      "<h3>" + movie + "</h3>" +
      '<button class="fav-btn" data-movie="' + movie + '">' +
      (isFav ? "remove from favorites" : "add to favorites") +
      "</button>";
    moviesGrid.appendChild(card);
  });
}

// delegated handler for favorite buttons
moviesGrid.addEventListener("click", function (event) {
  let btn = event.target.closest(".fav-btn");
  if (!btn) return;

  let movie = btn.dataset.movie;
  let favorites = getFavorites();
  let index = favorites.indexOf(movie);

  if (index === -1) {
    favorites.push(movie);
  } else {
    favorites.splice(index, 1);
  }

  saveFavorites(favorites);
  renderMovies();
});

// clear all
document.getElementById("clear-favs").addEventListener("click", function () {
  saveFavorites([]);
  renderMovies();
});

// initial render
renderMovies();
  • מערך המועדפים נשמר כ-JSON ב-localStorage
  • includes בודק אם סרט כבר במועדפים
  • splice מוציא פריט מהמערך לפי אינדקס
  • בכל שינוי, שומרים ומרנדרים מחדש

תרגיל 5 - טופס עם שמירה אוטומטית

<form id="contact-form">
  <input type="text" name="name" placeholder="name">
  <input type="email" name="email" placeholder="email">
  <select name="subject">
    <option value="">choose subject...</option>
    <option value="general">general</option>
    <option value="support">support</option>
    <option value="feedback">feedback</option>
  </select>
  <textarea name="message" placeholder="message"></textarea>
  <button type="submit">send</button>
  <button type="button" id="clear-draft">clear draft</button>
</form>
<div id="draft-msg" style="color: green; display: none;">draft saved</div>
let form = document.getElementById("contact-form");
let draftMsg = document.getElementById("draft-msg");
let draftKey = "contactFormDraft";

// save draft on every input change
form.addEventListener("input", function () {
  let data = Object.fromEntries(new FormData(form));
  localStorage.setItem(draftKey, JSON.stringify(data));

  // show "draft saved" message
  draftMsg.style.display = "block";
  setTimeout(function () {
    draftMsg.style.display = "none";
  }, 2000);
});

// restore draft on page load
function restoreDraft() {
  let saved = localStorage.getItem(draftKey);
  if (!saved) return;

  let data = JSON.parse(saved);
  Object.keys(data).forEach(function (key) {
    let field = form.elements[key];
    if (field) {
      field.value = data[key];
    }
  });
}

// clear draft
document.getElementById("clear-draft").addEventListener("click", function () {
  localStorage.removeItem(draftKey);
  form.reset();
});

// on submit - clear draft
form.addEventListener("submit", function (event) {
  event.preventDefault();
  localStorage.removeItem(draftKey);
  form.reset();
  alert("message sent!");
});

// restore on load
restoreDraft();
  • אירוע input על ה-form מתפשט מכל שדה ילד (בעבוע)
  • FormData + Object.fromEntries אוספים את כל הנתונים בשורה אחת
  • בטעינה, מחזירים את הנתונים לשדות
  • בשליחה או ניקוי, מוחקים את הטיוטה

תרגיל 6 - יומן רשומות

<textarea id="entry-input" placeholder="write your entry..."></textarea>
<button id="add-entry">add entry</button>
<input type="text" id="search-entries" placeholder="search entries...">
<p id="entry-count">0 entries</p>
<div id="entries-list"></div>
let entryInput = document.getElementById("entry-input");
let searchInput = document.getElementById("search-entries");
let entriesList = document.getElementById("entries-list");
let entryCount = document.getElementById("entry-count");

function getEntries() {
  return JSON.parse(localStorage.getItem("journal")) || [];
}

function saveEntries(entries) {
  localStorage.setItem("journal", JSON.stringify(entries));
}

function renderEntries(filter) {
  let entries = getEntries();
  let filtered = entries;

  if (filter) {
    let query = filter.toLowerCase();
    filtered = entries.filter(function (entry) {
      return entry.text.toLowerCase().includes(query);
    });
  }

  entryCount.textContent = filtered.length + " entries";
  entriesList.innerHTML = "";

  filtered.forEach(function (entry, index) {
    let div = document.createElement("div");
    div.className = "entry";
    div.innerHTML =
      "<small>" + entry.date + "</small>" +
      "<p>" + entry.text + "</p>" +
      '<button class="delete-entry" data-index="' + index + '">delete</button>';
    entriesList.appendChild(div);
  });
}

// add entry
document.getElementById("add-entry").addEventListener("click", function () {
  let text = entryInput.value.trim();
  if (!text) return;

  let entries = getEntries();
  let now = new Date();
  let dateStr = now.toLocaleDateString() + " " + now.toLocaleTimeString();

  // add to beginning (newest first)
  entries.unshift({
    text: text,
    date: dateStr
  });

  saveEntries(entries);
  entryInput.value = "";
  renderEntries(searchInput.value);
});

// delete entry (delegated)
entriesList.addEventListener("click", function (event) {
  let btn = event.target.closest(".delete-entry");
  if (!btn) return;

  let index = parseInt(btn.dataset.index);
  let entries = getEntries();
  entries.splice(index, 1);
  saveEntries(entries);
  renderEntries(searchInput.value);
});

// search
searchInput.addEventListener("input", function () {
  renderEntries(searchInput.value);
});

// initial render
renderEntries();
  • unshift מוסיף לתחילת המערך (רשומות חדשות למעלה)
  • data-index על כפתור המחיקה מאפשר לזהות את הרשומה הנכונה
  • toLocaleDateString ו-toLocaleTimeString מפרמטים תאריך קריא

תרגיל 7 - Storage Wrapper

let storage = {
  get: function (key, defaultValue) {
    let data = localStorage.getItem(key);
    if (data === null) {
      return defaultValue !== undefined ? defaultValue : null;
    }
    try {
      return JSON.parse(data);
    } catch (e) {
      return data; // return raw string if not valid JSON
    }
  },

  set: function (key, value) {
    localStorage.setItem(key, JSON.stringify(value));
  },

  remove: function (key) {
    localStorage.removeItem(key);
  },

  has: function (key) {
    return localStorage.getItem(key) !== null;
  },

  clear: function () {
    localStorage.clear();
  },

  getAll: function () {
    let all = {};
    for (let i = 0; i < localStorage.length; i++) {
      let key = localStorage.key(i);
      all[key] = this.get(key);
    }
    return all;
  }
};

שימוש ב-wrapper לבניית רשימת משימות:

<input type="text" id="task-input" placeholder="new task...">
<button id="add-task">add</button>
<ul id="task-list"></ul>
function getTasks() {
  return storage.get("tasks", []);
}

function renderTasks() {
  let tasks = getTasks();
  let list = document.getElementById("task-list");
  list.innerHTML = "";

  tasks.forEach(function (task, index) {
    let li = document.createElement("li");
    let doneClass = task.done ? "done" : "";
    li.innerHTML =
      '<span class="' + doneClass + '">' + task.text + "</span> " +
      '<button class="toggle-btn" data-index="' + index + '">' +
      (task.done ? "undo" : "done") +
      "</button> " +
      '<button class="delete-btn" data-index="' + index + '">delete</button>';
    list.appendChild(li);
  });
}

document.getElementById("add-task").addEventListener("click", function () {
  let input = document.getElementById("task-input");
  let text = input.value.trim();
  if (!text) return;

  let tasks = getTasks();
  tasks.push({ text: text, done: false });
  storage.set("tasks", tasks);
  input.value = "";
  renderTasks();
});

document.getElementById("task-list").addEventListener("click", function (event) {
  let tasks = getTasks();

  let toggleBtn = event.target.closest(".toggle-btn");
  if (toggleBtn) {
    let index = parseInt(toggleBtn.dataset.index);
    tasks[index].done = !tasks[index].done;
    storage.set("tasks", tasks);
    renderTasks();
    return;
  }

  let deleteBtn = event.target.closest(".delete-btn");
  if (deleteBtn) {
    let index = parseInt(deleteBtn.dataset.index);
    tasks.splice(index, 1);
    storage.set("tasks", tasks);
    renderTasks();
    return;
  }
});

renderTasks();
  • ה-wrapper מטפל ב-JSON אוטומטית
  • get עם defaultValue מחזיר מערך ריק אם אין נתונים
  • try/catch ב-get מגן מפני JSON לא תקין
  • getAll עובר על כל ה-keys ומחזיר אובייקט מלא

תרגיל 8 - תקשורת בין טאבים

<input type="text" id="msg-input" placeholder="type a message...">
<button id="send-btn">send</button>
<button id="clear-history">clear history</button>
<h3>received messages:</h3>
<div id="received-messages"></div>
let msgInput = document.getElementById("msg-input");
let receivedDiv = document.getElementById("received-messages");

function getHistory() {
  return JSON.parse(localStorage.getItem("chatHistory")) || [];
}

function renderReceived() {
  let history = getHistory();
  receivedDiv.innerHTML = "";

  history.forEach(function (msg) {
    let div = document.createElement("div");
    div.className = "message";
    div.innerHTML = "<small>" + msg.time + "</small><p>" + msg.text + "</p>";
    receivedDiv.appendChild(div);
  });
}

// send message
document.getElementById("send-btn").addEventListener("click", function () {
  let text = msgInput.value.trim();
  if (!text) return;

  let now = new Date();
  let time = now.toLocaleTimeString();

  let history = getHistory();
  history.push({ text: text, time: time });
  localStorage.setItem("chatHistory", JSON.stringify(history));

  // also update a "lastMessage" key to trigger the storage event
  localStorage.setItem("lastMessage", JSON.stringify({
    text: text,
    time: time,
    id: Date.now() // unique id to ensure value changes
  }));

  msgInput.value = "";
});

// listen for changes from OTHER tabs
window.addEventListener("storage", function (event) {
  if (event.key === "lastMessage" && event.newValue) {
    // a new message was sent from another tab
    renderReceived();
  }

  if (event.key === "chatHistory" && event.newValue === null) {
    // history was cleared from another tab
    renderReceived();
  }
});

// clear history
document.getElementById("clear-history").addEventListener("click", function () {
  localStorage.removeItem("chatHistory");
  localStorage.removeItem("lastMessage");
  renderReceived();
});

// initial render
renderReceived();
  • lastMessage key נפרד משמש להפעלת אירוע storage (כי chatHistory עשוי להיות ארוך)
  • Date.now() כ-id מבטיח שהערך תמיד משתנה (אחרת storage event לא מופעל אם הערך זהה)
  • אירוע storage מופעל רק בטאבים אחרים, לא בטאב ששינה את הערך
  • פתחו את הדף בשני טאבים ושלחו הודעה מאחד - היא תופיע בשני

תרגיל 9 - מערכת ציונים גבוהים

<div>
  <input type="text" id="player-name" placeholder="player name">
  <input type="number" id="player-score" placeholder="score">
  <button id="add-score">add score</button>
  <button id="reset-scores">reset scores</button>
</div>
<table id="scores-table">
  <thead>
    <tr>
      <th>#</th>
      <th>name</th>
      <th>score</th>
      <th>date</th>
    </tr>
  </thead>
  <tbody></tbody>
</table>
.new-score {
  background: #d4edda;
  animation: highlight 2s ease;
}

@keyframes highlight {
  from { background: #a8e6cf; }
  to { background: transparent; }
}
let nameInput = document.getElementById("player-name");
let scoreInput = document.getElementById("player-score");
let tbody = document.querySelector("#scores-table tbody");
let lastAddedId = null;

function getScores() {
  return JSON.parse(localStorage.getItem("highScores")) || [];
}

function saveScores(scores) {
  localStorage.setItem("highScores", JSON.stringify(scores));
}

function renderScores() {
  let scores = getScores();
  tbody.innerHTML = "";

  scores.forEach(function (entry, index) {
    let tr = document.createElement("tr");
    if (entry.id === lastAddedId) {
      tr.className = "new-score";
    }
    tr.innerHTML =
      "<td>" + (index + 1) + "</td>" +
      "<td>" + entry.name + "</td>" +
      "<td>" + entry.score + "</td>" +
      "<td>" + entry.date + "</td>";
    tbody.appendChild(tr);
  });
}

document.getElementById("add-score").addEventListener("click", function () {
  let name = nameInput.value.trim();
  let score = parseInt(scoreInput.value);
  if (!name || isNaN(score)) return;

  let scores = getScores();
  let id = Date.now();

  scores.push({
    id: id,
    name: name,
    score: score,
    date: new Date().toLocaleDateString()
  });

  // sort by score descending
  scores.sort(function (a, b) {
    return b.score - a.score;
  });

  // keep only top 10
  scores = scores.slice(0, 10);

  saveScores(scores);
  lastAddedId = id;
  renderScores();

  nameInput.value = "";
  scoreInput.value = "";
});

document.getElementById("reset-scores").addEventListener("click", function () {
  localStorage.removeItem("highScores");
  lastAddedId = null;
  renderScores();
});

// initial render
renderScores();
  • sort עם comparator ממיין מהגבוה לנמוך
  • slice(0, 10) שומר רק את 10 הציונים הגבוהים
  • Date.now() כ-id ייחודי מאפשר לזהות את הציון שנוסף אחרון
  • CSS animation מדגיש את השורה החדשה עם אפקט fade