0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ローカルストレージで永続的保存

Last updated at Posted at 2025-06-14

なぜFormDataオブジェクトを使ってるのか?

そうした方が保存しやすいから。
理屈: 入力form全体のidからdata取得し、formオブジェクトにdataを入れてる

2種類配列を用意しなきゃいけない

テーブル表示用配列:LOCAL_STORAGE_KEY_TABLE_DATA

入力form用配列:LOCAL_STORAGE_KEY_FORM_DRAFT

dataが保存されたら、入力formのdataをすぐ消去

//dataが保存されたら、入力formのdataをすぐ消去
localStorage.removeItem(LOCAL_STORAGE_KEY_FORM_DRAFT);

付け足す設定

入力formに、name属性をつける

formオブジェクトを使う

formオブジェクト用の配列を用意する
formオブジェクトにdataを入れるときは、keyと値がワンセット
入力formにname属性をつける
<input type="email" id="editEmail" name="editEmail">

formオブジェクトを使う
const formData = new FormData(formElement);

formオブジェクト用の配列を用意
const dataToSave = {};

entries : 変数を指定しセットで取り出す
 keyと値をセットで保存
for (let [key, value] of formData.entries()) {
        dataToSave[key] = value;
      }

Localstorageで保存しない方がいいもの

パスワード

クレジットカード

セッションデータ

マイナンバーカードとか

code全文

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>テーブル表示練習</title>
  <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
  <style>
    body {
        font-family: Arial, sans-serif;
        margin: 20px;
    }
    .form-group { margin-bottom: 10px; }
    .label { display: inline-block; width: 80px; }
    .error-message { display:none; color:red; margin-left:5px; }

    /* テーブル全体の基本スタイル */
    table {
        width: 100%;
        border-collapse: collapse;
        margin-top: 20px;
    }
    table, th, td { border:1px solid #000; border-collapse:collapse; padding:5px; }

    /* テーブルヘッダーの枠線を太くする */
    th {
        border: 3px solid #000; /* ヘッダーの枠線を太く */
    }

    /* ボタンの初期スタイル */
    #addButon {
        padding: 8px 15px;
        font-size: 16px;
        cursor: pointer;
        background-color: #007bff; /* 初期の色(青) */
        color: white;
        border: none;
        border-radius: 4px;
        transition: background-color 0.3s ease; /* 色の変化を滑らかに */
    }

    /* ボタンがクリックされた後のスタイル */
    #addButon.clicked-button {
        background-color: #28a745; /* クリック後は別の色(例: 緑) */
    }

    /* 削除・編集ボタンのスタイル */
    .action-button {
        padding: 5px 10px;
        margin-left: 5px; /* ボタン間のスペース */
        border: none;
        border-radius: 3px;
        cursor: pointer;
        font-size: 13px;
        color: white;
    }

    .delete-button {
        background-color: #dc3545; /* 赤色 */
    }

    .edit-button {
        background-color: #ffc107; /* 黄色 */
        color: black; /* 黄色には黒文字が見やすい */
    }

    /* --- モーダルウィンドウのスタイル --- */
    .modal {
      display: none; /* 初期状態では非表示 */
      position: fixed; /* 画面に固定 */
      z-index: 1; /* 最前面に表示 */
      left: 0;
      top: 0;
      width: 100%; /* 全幅 */
      height: 100%; /* 全高 */
      overflow: auto; /* スクロール可能にする */
      background-color: rgba(0,0,0,0.4); /* 半透明の黒背景 */
      justify-content: center;
      align-items: center;
    }

    .modal-content {
      background-color: #fefefe;
      margin: auto; /* 中央寄せ */
      padding: 20px;
      border: 1px solid #888;
      width: 80%; /* 幅 */
      max-width: 500px; /* 最大幅 */
      border-radius: 8px;
      box-shadow: 0 4px 8px rgba(0,0,0,0.2);
      position: relative;
    }

    .close-button {
      color: #aaa;
      float: right;
      font-size: 28px;
      font-weight: bold;
    }

    .close-button:hover,
    .close-button:focus {
      color: black;
      text-decoration: none;
      cursor: pointer;
    }

    .modal-buttons {
      margin-top: 20px;
      text-align: right;
    }
    .modal-buttons button {
      padding: 8px 15px;
      margin-left: 10px;
      border: none;
      border-radius: 4px;
      cursor: pointer;
    }
    .modal-buttons .save-button {
      background-color: #28a745;
      color: white;
    }
    .modal-buttons .cancel-button {
      background-color: #6c757d;
      color: white;
    }
  </style>
</head>
<body>

<form id="userForm">
  <div class="form-group">
    <label class="label" for="studentName">名前</label>
    <input type="text" id="studentName" name="studentName">
    <div id="nameError" class="error-message">名前を入力してください</div>
  </div>
  <div class="form-group">
    <label class="label" for="studentAge">年齢</label>
    <input type="number" id="studentAge" name="studentAge">
  </div>
  <div class="form-group">
    <label class="label" for="studentEmail">メール</label>
    <input type="email" id="studentEmail" name="studentEmail">
  </div>
  <div class="form-group">
    <label class="label" for="studentnumber">電話</label>
    <input type="text" id="studentnumber" name="studentnumber">
  </div>
  <button type="button" id="addButon">追加</button>
</form>

<hr>

<table id="studentTable">
  <thead>
    <tr><th>ID</th><th>名前</th><th>年齢</th><th>メール</th><th>電話</th><th>操作</th></tr> </thead>
  <tbody></tbody>
</table>

<div id="editModal" class="modal">
  <div class="modal-content">
    <span class="close-button">&times;</span>
    <h2>データ編集</h2>
    <input type="hidden" id="editId">
    <div class="form-group">
      <label class="label" for="editName">名前</label>
      <input type="text" id="editName" name="editName">
    </div>
    <div class="form-group">
      <label class="label" for="editAge">年齢</label>
      <input type="number" id="editAge" name="editAge">
    </div>
    <div class="form-group">
      <label class="label" for="editEmail">メール</label>
      <input type="email" id="editEmail" name="editEmail">
    </div>
    <div class="form-group">
      <label class="label" for="editPhone">電話</label>
      <input type="text" id="editPhone" name="editPhone">
    </div>
    <div class="modal-buttons">
      <button class="save-button" id="saveEditButton">保存</button>
      <button class="cancel-button" id="cancelEditButton">キャンセル</button>
    </div>
  </div>
</div>

<script>
document.addEventListener('DOMContentLoaded', () => {
  const LOCAL_STORAGE_KEY_TABLE_DATA = 'studentData'; // テーブルデータのローカルストレージキー
  const LOCAL_STORAGE_KEY_FORM_DRAFT = 'userFormDraft'; // フォーム入力値のローカルストレージキー

  const userForm = document.getElementById('userForm'); // フォーム要素全体を取得
  const addButton = document.getElementById('addButon');
  const nameInput = document.getElementById('studentName');
  const ageInput = document.getElementById('studentAge');
  const emailInput = document.getElementById('studentEmail');
  const phoneInput = document.getElementById('studentnumber');
  const nameError = document.getElementById('nameError');
  const tableBody = document.querySelector('#studentTable tbody');

  // モーダル関連要素
  const editModal = document.getElementById('editModal');
  const closeButton = document.querySelector('.close-button');
  const saveEditButton = document.getElementById('saveEditButton');
  const cancelEditButton = document.getElementById('cancelEditButton');
  const editIdInput = document.getElementById('editId');
  const editNameInput = document.getElementById('editName');
  const editAgeInput = document.getElementById('editAge');
  const editEmailInput = document.getElementById('editEmail');
  const editPhoneInput = document.getElementById('editPhone');

  let studentData = []; // アプリケーションのデータ配列
  let nextId = 1; // 次に割り当てるID

  // --- ローカルストレージとデータ管理 ---

  // ローカルストレージからテーブルデータを読み込む
  function loadTableDataFromLocalStorage() {
    const storedData = localStorage.getItem(LOCAL_STORAGE_KEY_TABLE_DATA);
    if (storedData) {
      studentData = JSON.parse(storedData);
      // 既存データから最大のIDを見つけてnextIdを設定
      if (studentData.length > 0) {
        nextId = Math.max(...studentData.map(item => item.id)) + 1;
      }
      renderTable(); // テーブルを初期表示
    }
  }

  // ローカルストレージにテーブルデータを保存する
  function saveTableDataToLocalStorage() {
    localStorage.setItem(LOCAL_STORAGE_KEY_TABLE_DATA, JSON.stringify(studentData));
  }

  // フォーム入力値をローカルストレージに自動保存する
  function setupAutoSaveForm(formElement, localStorageKey) {
    formElement.addEventListener('input', () => {
      const formData = new FormData(formElement);
      const dataToSave = {};
      for (let [key, value] of formData.entries()) {
        dataToSave[key] = value;
      }
      localStorage.setItem(localStorageKey, JSON.stringify(dataToSave));
      // console.log('フォームの入力値をローカルストレージに自動保存しました:', dataToSave);
    });
  }

  // ローカルストレージからフォームの入力値をロードしてフォームに設定する
  function loadAndFillForm(formElement, localStorageKey) {
    const savedData = localStorage.getItem(localStorageKey);
    if (savedData) {
      const data = JSON.parse(savedData);
      for (const key in data) {
        // name属性が一致するinput要素を探して値を設定
        const inputElement = formElement.querySelector(`[name="${key}"]`);
        if (inputElement) {
          inputElement.value = data[key];
        }
      }
      // console.log('ローカルストレージからフォームの入力値をロードしました:', data);
    }
  }

  // --- 関数定義 ---

  // 入力データ取得
  function getFormData() {
    return {
      id: nextId, // 現在のnextIdを割り当てる
      name: nameInput.value.trim(),
      age: parseInt(ageInput.value.trim()) || 0, // 数値に変換、無効な場合は0
      email: emailInput.value.trim(),
      phone: phoneInput.value.trim()
    };
  }

  // バリデーション
  function validateForm(data) {
    if (data.name === '') {
      nameError.style.display = 'inline';
      setTimeout(() => { nameError.style.display = 'none'; }, 2000);
      return false;
    }
    if (isNaN(data.age) || data.age <= 0) {
      alert('正しい年齢を入力してください');
      return false;
    }
    return true;
  }

  // バリデーションエラー初期化
  function clearErrors() {
    nameError.style.display = 'none';
  }

  // --- CRUD操作(Axiosを使用) ---

  // データ新規追加 (Create)
  async function createData(data) {
    try {
      const response = await axios.post('https://jsonplaceholder.typicode.com/users', data); // ダミーAPI
      console.log('APIから新規データが追加されました:', response.data);
      
      // 成功したらローカルデータとLocalStorageを更新
      studentData.push({ ...data, id: nextId++ }); // IDを更新
      saveTableDataToLocalStorage();
      renderTable();
      clearFormInputs();
      localStorage.removeItem(LOCAL_STORAGE_KEY_FORM_DRAFT); // フォーム送信成功後、下書きをクリア
    } catch (error) {
      console.error('データ追加に失敗しました:', error);
      alert('データ追加に失敗しました。');
    }
  }

  // データ編集 (Update)
  async function updateData(id, updatedData) {
    try {
      // ダミーAPIはPUT/PATCHしても実際のデータは更新しないため、擬似的な成功とします
      const response = await axios.put(`https://jsonplaceholder.typicode.com/users/${id}`, updatedData); // ダミーAPI
      console.log('APIでデータが更新されました:', response.data);

      // 成功したらローカルデータとLocalStorageを更新
      const index = studentData.findIndex(item => item.id === id);
      if (index !== -1) {
        studentData[index] = { ...studentData[index], ...updatedData };
        saveTableDataToLocalStorage();
        renderTable();
      }
      closeEditModal(); // 編集成功後にモーダルを閉じる
    } catch (error) {
      console.error('データ更新に失敗しました:', error);
      alert('データ更新に失敗しました。');
    }
  }

  // データ削除 (Delete)
  async function deleteData(id) {
    const confirmDelete = confirm('本当に削除しますか?');
    if (!confirmDelete) return;

    try {
      await axios.delete(`https://jsonplaceholder.typicode.com/users/${id}`); // ダミーAPI
      console.log(`APIからID ${id} のデータが削除されました。`);

      // 成功したらローカルデータとLocalStorageを更新
      studentData = studentData.filter(item => item.id !== id);
      saveTableDataToLocalStorage();
      renderTable();
    } catch (error) {
      console.error('データ削除に失敗しました:', error);
      alert('データ削除に失敗しました。');
    }
  }

  // --- UI操作 ---

  // テーブルに行追加(編集・削除ボタン付き)
  function addTableRow(data) {
    const row = document.createElement('tr');
    row.setAttribute('data-id', data.id); // 行にデータのIDを設定
    row.innerHTML = `
      <td>${data.id}</td>
      <td>${data.name}</td>
      <td>${data.age}</td>
      <td>${data.email}</td>
      <td>${data.phone}</td>
      <td>
        <button class="action-button edit-button">編集</button>
        <button class="action-button delete-button">削除</button>
      </td>
    `;
    tableBody.appendChild(row);

    // 追加したばかりのボタンにイベントリスナーを設定
    const editButton = row.querySelector('.edit-button');
    const deleteButton = row.querySelector('.delete-button');

    editButton.addEventListener('click', () => {
      openEditModal(data.id); // 編集モーダルを開く
    });

    deleteButton.addEventListener('click', () => {
      deleteData(data.id); // データ削除関数を呼び出す
    });
  }

  // 入力欄クリア
  function clearFormInputs() {
    nameInput.value = '';
    ageInput.value = '';
    emailInput.value = '';
    phoneInput.value = '';
  }

  // テーブルを再描画する関数
  function renderTable() {
    tableBody.innerHTML = ''; // テーブルの中身をクリア
    studentData.forEach(data => addTableRow(data)); // 全てのデータを再描画
  }

  // 編集モーダルを開く関数
  function openEditModal(id) {
    const dataToEdit = studentData.find(item => item.id === id);
    if (dataToEdit) {
      editIdInput.value = dataToEdit.id;
      editNameInput.value = dataToEdit.name;
      editAgeInput.value = dataToEdit.age;
      editEmailInput.value = dataToEdit.email;
      editPhoneInput.value = dataToEdit.phone;
      editModal.style.display = 'flex'; // flexにして中央寄せを有効に
    }
  }

  // 編集モーダルを閉じる関数
  function closeEditModal() {
    editModal.style.display = 'none';
  }

  // --- イベントリスナー ---

  // ページロード時にローカルストレージからテーブルデータを読み込み、テーブルを表示
  loadTableDataFromLocalStorage();

  // フォームの入力値を自動保存する機能をセットアップ
  setupAutoSaveForm(userForm, LOCAL_STORAGE_KEY_FORM_DRAFT);
  // ページロード時にローカルストレージからフォームの値をロードして入力欄に設定
  loadAndFillForm(userForm, LOCAL_STORAGE_KEY_FORM_DRAFT);

  // 「追加」ボタンのクリックイベント
  addButton.addEventListener('click', () => {
    // ボタンの色を青に変更 (今回はクリック後に緑に変える例)
    addButton.classList.add('clicked-button'); 

    const formData = getFormData();
    clearErrors();

    if (!validateForm(formData)) return;

    createData(formData); // データ追加関数を呼び出す
  });

  // モーダルを閉じるボタン
  closeButton.addEventListener('click', closeEditModal);
  cancelEditButton.addEventListener('click', closeEditModal);

  // モーダル内の保存ボタン
  saveEditButton.addEventListener('click', () => {
    const id = parseInt(editIdInput.value);
    const updatedData = {
      name: editNameInput.value.trim(),
      age: parseInt(editAgeInput.value.trim()),
      email: editEmailInput.value.trim(),
      phone: editPhoneInput.value.trim()
    };

    if (!validateForm(updatedData)) return; // 編集フォームのバリデーションも既存関数を利用

    updateData(id, updatedData); // データ更新関数を呼び出す
  });
});
</script>

</body>
</html>
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?