JavaScript | 1 日 120 分 × 7 日アプリ学習:ローカル保存対応メモ帳

APP JavaScript
スポンサーリンク

2日目のゴールと今日のテーマ

2日目は「実際に“消えないメモ帳”を動く形にする日」です。
1日目で学んだのは、localStorage と JSON の基本的な仕組みでした。今日はそれを使って、

メモを追加できる。
メモを編集できる。
メモを削除できる。
ページをリロードしてもメモが残っている。

というところまで持っていきます。
特に大事なのは、「どのタイミングで localStorage に保存するか」という“保存タイミングの設計”です。


アプリ全体の構成をざっくり決める

画面のイメージを言葉で描く

まずは、どんな画面にするかを言葉で整理します。

上の方に「新規メモ入力欄」と「追加ボタン」がある。
その下に「メモ一覧」が縦に並ぶ。
各メモには「内容」と「編集ボタン」と「削除ボタン」が付いている。
編集ボタンを押すと、そのメモだけ入力欄に変わる。

このくらいのイメージが頭にあれば十分です。
ToDo アプリと同じく、「1 メモ = 1 行」という構造で考えます。

HTML の最小構成を用意する

次のような HTML を用意します。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8" />
  <title>ローカル保存対応メモ帳</title>
  <style>
    body { font-family: sans-serif; padding: 20px; background: #f8f9fa; }
    .app-title { font-size: 20px; margin-bottom: 16px; }
    .memo-input-row { margin-bottom: 16px; }
    .memo-input-row textarea { width: 100%; height: 60px; padding: 8px; }
    .memo-input-row button { margin-top: 8px; padding: 4px 10px; }
    .memos-container { margin-top: 16px; }
    .memo-item { border: 1px solid #ddd; background: #fff; padding: 8px; margin-bottom: 8px; }
    .memo-text { white-space: pre-wrap; }
    .memo-actions { margin-top: 8px; text-align: right; }
    .memo-actions button { margin-left: 6px; }
  </style>
</head>
<body>
  <div class="app">
    <div class="app-title">ローカル保存対応メモ帳</div>

    <div class="memo-input-row">
      <textarea id="new-memo-input" placeholder="メモを入力..."></textarea>
      <button id="add-memo-button">追加</button>
    </div>

    <div id="memos-container" class="memos-container"></div>
  </div>

  <script src="app.js"></script>
</body>
</html>

ここで押さえておきたいのは、

新規メモ入力欄は <textarea> にして、複数行も書けるようにしていること。
メモ一覧は <div id="memos-container"> の中に JavaScript で差し込むこと。
1 メモは .memo-item という 1 つのブロックとして扱うこと。

この「構造のイメージ」が、後の state 設計ときれいにつながります。


state と localStorage の関係をはっきりさせる

state は「今のメモ一覧」を持つ

app.js で、まずは state と localStorage のキーを定義します。

const STORAGE_KEY = "local-memo-app";

const state = {
  memos: [],
  editingMemoId: null,
};
JavaScript

ここでのポイントは、

memos が「アプリが扱う全メモ」の配列であること。
editingMemoId が「今どのメモを編集しているか」を表す UI 状態であること。

localStorage はあくまで「保存場所」であって、
アプリの“今”は state に持たせる、という考え方が大事です。

保存と読み込みの関数を先に作る

まず、「保存」と「読み込み」を担当する関数を用意します。

function saveMemosToStorage() {
  const json = JSON.stringify(state.memos);
  localStorage.setItem(STORAGE_KEY, json);
}

function loadMemosFromStorage() {
  const json = localStorage.getItem(STORAGE_KEY);
  if (!json) {
    state.memos = [];
    return;
  }

  try {
    const memos = JSON.parse(json);
    if (Array.isArray(memos)) {
      state.memos = memos;
    } else {
      state.memos = [];
    }
  } catch (e) {
    console.error("メモの読み込みに失敗しました", e);
    state.memos = [];
  }
}
JavaScript

ここで深掘りしたいのは、

JSON.parse は失敗する可能性があるので try-catch で守っていること。
localStorage の中身が壊れていても、アプリが落ちないようにしていること。

これが「永続化設計」の大事な視点です。
「保存されているデータは、必ずしもきれいとは限らない」という前提で設計します。


メモ一覧の描画ロジックを作る

render の基本構造を作る

ToDo アプリと同じように、render 関数を用意します。

const memosContainerEl = document.getElementById("memos-container");

function render() {
  memosContainerEl.innerHTML = "";

  if (state.memos.length === 0) {
    const emptyEl = document.createElement("div");
    emptyEl.textContent = "メモはまだありません。最初のメモを書いてみましょう。";
    emptyEl.style.color = "#777";
    emptyEl.style.fontSize = "12px";
    memosContainerEl.appendChild(emptyEl);
    return;
  }

  state.memos.forEach((memo) => {
    const memoEl = renderMemoItem(memo);
    memosContainerEl.appendChild(memoEl);
  });
}
JavaScript

ここでのポイントは、

メモが 0 件のときに「何もない」ではなく、メッセージを出していること。
1 件のメモの描画を renderMemoItem に分けていること。

「1 関数 1 役割」に近づけると、読みやすさが一気に上がります。

1 件のメモを描画する関数を作る

function renderMemoItem(memo) {
  const itemEl = document.createElement("div");
  itemEl.className = "memo-item";

  const isEditing = state.editingMemoId === memo.id;

  if (isEditing) {
    renderMemoItemEditing(itemEl, memo);
  } else {
    renderMemoItemDisplay(itemEl, memo);
  }

  return itemEl;
}
JavaScript

ここで、「表示モード」と「編集モード」を分ける設計にしています。
これは ToDo の編集機能と同じパターンです。


メモの追加機能を実装する

新規メモ入力欄とボタンを取得する

const newMemoInputEl = document.getElementById("new-memo-input");
const addMemoButtonEl = document.getElementById("add-memo-button");
JavaScript

メモ追加のロジックを作る

function addMemo(text) {
  const trimmed = text.trim();
  if (!trimmed) return;

  const newMemo = {
    id: Date.now(),
    text: trimmed,
  };

  state.memos.push(newMemo);
  saveMemosToStorage();
  render();
}
JavaScript

ここでの重要ポイントは、

id に Date.now() を使って、一意な値を作っていること。
state.memos を更新したら、必ず saveMemosToStorage と render を呼んでいること。

「状態を変えたら保存と再描画」というセットを徹底することで、
画面と保存内容のズレを防ぎます。

ボタンと Enter キーにイベントを付ける

addMemoButtonEl.addEventListener("click", () => {
  addMemo(newMemoInputEl.value);
  newMemoInputEl.value = "";
});

newMemoInputEl.addEventListener("keydown", (event) => {
  if (event.key === "Enter" && (event.metaKey || event.ctrlKey)) {
    addMemo(newMemoInputEl.value);
    newMemoInputEl.value = "";
  }
});
JavaScript

ここでは、
通常の Enter は改行として使い、
Ctrl+Enter(または Cmd+Enter)で追加、という UX にしています。

「テキストエリアで Enter を押したら改行したい」という自然な動きと、
「ショートカットで追加したい」という両方を満たす設計です。


メモの表示モードと編集モードを実装する

表示モードの描画

function renderMemoItemDisplay(itemEl, memo) {
  const textEl = document.createElement("div");
  textEl.className = "memo-text";
  textEl.textContent = memo.text;

  const actionsEl = document.createElement("div");
  actionsEl.className = "memo-actions";

  const editButton = document.createElement("button");
  editButton.textContent = "編集";
  editButton.addEventListener("click", () => {
    startEditMemo(memo.id);
  });

  const deleteButton = document.createElement("button");
  deleteButton.textContent = "削除";
  deleteButton.addEventListener("click", () => {
    deleteMemo(memo.id);
  });

  actionsEl.appendChild(editButton);
  actionsEl.appendChild(deleteButton);

  itemEl.appendChild(textEl);
  itemEl.appendChild(actionsEl);
}
JavaScript

ここでのポイントは、

編集ボタンと削除ボタンに、それぞれ memo.id を渡していること。
「どのメモを操作するか」を id で特定する設計にしていること。

これは ToDo アプリとまったく同じ考え方です。

編集モードの描画

function renderMemoItemEditing(itemEl, memo) {
  const textareaEl = document.createElement("textarea");
  textareaEl.value = memo.text;
  textareaEl.style.width = "100%";
  textareaEl.style.height = "80px";

  const actionsEl = document.createElement("div");
  actionsEl.className = "memo-actions";

  const saveButton = document.createElement("button");
  saveButton.textContent = "保存";

  const cancelButton = document.createElement("button");
  cancelButton.textContent = "キャンセル";

  saveButton.addEventListener("click", () => {
    updateMemoText(memo.id, textareaEl.value);
  });

  cancelButton.addEventListener("click", () => {
    finishEditMemo();
  });

  textareaEl.addEventListener("keydown", (event) => {
    if (event.key === "Escape") {
      finishEditMemo();
    } else if (event.key === "Enter" && (event.metaKey || event.ctrlKey)) {
      updateMemoText(memo.id, textareaEl.value);
    }
  });

  actionsEl.appendChild(saveButton);
  actionsEl.appendChild(cancelButton);

  itemEl.appendChild(textareaEl);
  itemEl.appendChild(actionsEl);

  setTimeout(() => {
    textareaEl.focus();
    textareaEl.select();
  }, 0);
}
JavaScript

ここで深掘りしたいのは、

編集モードでは「表示用の div」ではなく「textarea」を使っていること。
保存・キャンセル・Esc・Ctrl+Enter で自然な編集体験を作っていること。
setTimeout で描画後にフォーカスを当てていること。

特に最後のフォーカスは、
「編集ボタンを押したらすぐ書き始められる」という UX にとても効きます。


メモの編集・削除ロジックと保存タイミング

編集開始と終了の state 操作

function startEditMemo(id) {
  state.editingMemoId = id;
  render();
}

function finishEditMemo() {
  state.editingMemoId = null;
  render();
}
JavaScript

ここでの重要ポイントは、

「今どのメモが編集モードか」も state の一部として扱っていること。
描画側は state.editingMemoId を見て、「自分が編集モードかどうか」を判断していること。

モードを state で管理するのは、設計としてとても強いパターンです。

メモ内容の更新と保存

function updateMemoText(id, newText) {
  const trimmed = newText.trim();
  if (!trimmed) {
    finishEditMemo();
    return;
  }

  state.memos = state.memos.map((memo) => {
    if (memo.id !== id) return memo;
    return { ...memo, text: trimmed };
  });

  saveMemosToStorage();
  finishEditMemo();
}
JavaScript

ここでの深掘りポイントは、

空文字は許可せず、そのままキャンセル扱いにしていること。
配列の更新を「map で新しい配列を作る」形にしていること。
更新後に saveMemosToStorage と finishEditMemo(→ render)を呼んでいること。

「状態を変えたら保存と再描画」という流れを、
追加だけでなく編集にも徹底しています。

メモ削除と保存

function deleteMemo(id) {
  state.memos = state.memos.filter((memo) => memo.id !== id);
  saveMemosToStorage();
  render();
}
JavaScript

削除も同じく、

配列を直接いじるのではなく、filter で「残すものだけ」を残した新しい配列を作っていること。
削除後に saveMemosToStorage と render を呼んでいること。

これで、「画面に見えているもの」と「保存されているもの」が常に一致します。


2日目のまとめと、明日へのつなぎ

2日目であなたがやったのは、「localStorage を使った“消えないメモ帳”のコア部分」です。

state.memos を「アプリの真実」として持ち、localStorage は保存場所として扱ったこと。
JSON.stringify / JSON.parse を使って、配列を文字列として保存・復元したこと。
メモ追加・編集・削除のたびに saveMemosToStorage を呼ぶ「保存タイミングの設計」をしたこと。
編集モードを editingMemoId で管理し、表示モードと編集モードを切り替える設計にしたこと。

明日(3日目)はここから、

保存タイミングをもう少し工夫する(入力中に自動保存するか、しないか)。
「保存中」「保存済み」などのステータス表示を設計してみる。
localStorage のデータ構造を少しだけ拡張してみる。

といった、「永続化設計を一段深くする」方向に進めていきます。

もし余力があれば、2日目のうちに、

メモをいくつか追加して、ページをリロードしても残っているか確かめる。
localStorage の中身(Application タブ)を見て、JSON がどう保存されているか眺めてみる。

という“小さな確認”をしてみてください。
「ブラウザの中に、自分のメモがちゃんと残っている」という実感が、
永続化の感覚をぐっとリアルなものにしてくれます。

タイトルとURLをコピーしました