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

APP JavaScript
スポンサーリンク

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

4日目は「メモ帳の“中身の設計”を一段大人にする日」です。
ここまでで、メモは追加・編集・削除できて、localStorage にも保存され、ページを再読み込みしても残るようになりました。
今日はそこから一歩進めて、

メモのデータ構造を少しリッチにする(タイトル・本文・更新日時など)。
並び順(新しい順・古い順)を意識した設計にする。
localStorage に保存する“形”を意識して設計する。

という、「永続化されるデータの設計」にフォーカスしていきます。


メモのデータ構造を“ただの text”から卒業させる

1 件のメモを「タイトル+本文+更新日時」で考える

今までは、メモ 1 件をこう持っていました。

const memo = {
  id: 1,
  text: "今日のアイデアメモ",
};
JavaScript

これを、少しだけリッチにします。

const memo = {
  id: 1,
  title: "今日のアイデア",
  body: "・新しいサービス案\n・UI のラフ\n・やることメモ",
  updatedAt: Date.now(),
};
JavaScript

ここでのポイントは、役割を分けていることです。
タイトルは一覧でパッと見て内容を思い出すための短いテキスト。
本文は実際のメモ内容(複数行 OK)。
updatedAt は「いつ最後に触ったか」を表す数値(タイムスタンプ)です。

この形にしておくと、後で「更新日時順に並べる」「タイトルだけ一覧に出す」などの拡張がしやすくなります。

state の構造を更新する

state をこうしておきます。

const state = {
  memos: [
    // { id, title, body, updatedAt }
  ],
  editingMemoId: null,
  saveStatus: "idle",
};
JavaScript

ここで大事なのは、「localStorage に保存されるのは、この memos の中身そのもの」という意識です。
つまり、「永続化されるデータ構造 = state.memos の 1 件の形」と考えて設計していきます。


localStorage に保存される“形”を意識する

JSON にしたときの姿をイメージする

例えば、memos が 2 件あるとき、JSON.stringify するとこうなります。

const memos = [
  {
    id: 1,
    title: "今日のアイデア",
    body: "メモ本文...",
    updatedAt: 1736400000000,
  },
  {
    id: 2,
    title: "買い物リスト",
    body: "牛乳\nパン\n卵",
    updatedAt: 1736401000000,
  },
];

const json = JSON.stringify(memos);
JavaScript

json の中身は、ざっくりこんな文字列です。

[
  {"id":1,"title":"今日のアイデア","body":"メモ本文...","updatedAt":1736400000000},
  {"id":2,"title":"買い物リスト","body":"牛乳\nパン\n卵","updatedAt":1736401000000}
]

ここで意識してほしいのは、「localStorage に保存されるのはこの文字列」ということです。
つまり、「将来このデータをどう使いたいか」を考えながら、プロパティ名や構造を決めるのが“永続化設計”です。

読み込み時に「足りないプロパティ」を補う

既に保存済みのデータは、まだ title や updatedAt を持っていないかもしれません。
そこで、読み込み時に「足りないものを補う」処理を入れておきます。

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 = [];
      return;
    }

    state.memos = memos.map((memo, index) => {
      return {
        id: memo.id ?? Date.now() + index,
        title: memo.title ?? "無題のメモ",
        body: memo.body ?? memo.text ?? "",
        updatedAt: memo.updatedAt ?? Date.now(),
      };
    });
  } catch (e) {
    console.error("メモの読み込みに失敗しました", e);
    state.memos = [];
  }
}
JavaScript

ここでの重要ポイントは、「古い形式のデータも受け入れて、新しい形に変換している」ことです。
永続化されたデータは、アプリのバージョンアップをまたいで生き続けるので、「古いデータをどう扱うか」も設計の一部になります。


メモ追加・編集を「タイトル+本文」対応にする

新規メモ入力欄をタイトル+本文に分ける

HTML を少し変えます。

<div class="memo-input-row">
  <input id="new-memo-title" placeholder="タイトル(任意)" />
  <textarea id="new-memo-body" placeholder="メモ本文を入力..."></textarea>
  <button id="add-memo-button">追加</button>
</div>

JavaScript 側で要素を取得します。

const newMemoTitleEl = document.getElementById("new-memo-title");
const newMemoBodyEl = document.getElementById("new-memo-body");
JavaScript

メモ追加ロジックを更新する

function addMemo(title, body) {
  const trimmedTitle = title.trim();
  const trimmedBody = body.trim();

  if (!trimmedBody && !trimmedTitle) {
    return;
  }

  const now = Date.now();

  const newMemo = {
    id: now,
    title: trimmedTitle || "無題のメモ",
    body: trimmedBody,
    updatedAt: now,
  };

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

ここでの深掘りポイントは、いくつかあります。
タイトルも本文も空なら何もしない、というバリデーションを入れていること。
タイトルが空なら「無題のメモ」と自動で補っていること。
新しいメモを配列の先頭に unshift して、「新しい順」にしていること。
updatedAt に作成時刻を入れていること。

「新しい順に並べる」というのは、メモ帳としてかなり自然な UX です。
この時点で、並び順の設計も一歩進んでいます。


並び順の設計:updatedAt を使ってソートする

render 前に「並び順」を決める

今は unshift で「新しいものが先頭」に来るようにしていますが、
より明示的に「updatedAt の降順で並べる」という設計にしてみます。

function getSortedMemos() {
  return [...state.memos].sort((a, b) => b.updatedAt - a.updatedAt);
}
JavaScript

そして、render でこれを使います。

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

  const memos = getSortedMemos();

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

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

  renderSaveStatus();
}
JavaScript

ここでの重要ポイントは、「並び順のロジックを getSortedMemos に閉じ込めている」ことです。
これにより、「並び順を変えたい」と思ったときに、そこだけ触れば済みます。

編集時にも updatedAt を更新する

編集時の updateMemoText を、タイトル+本文対応にします。

function updateMemo(id, newTitle, newBody) {
  const trimmedTitle = newTitle.trim();
  const trimmedBody = newBody.trim();

  if (!trimmedTitle && !trimmedBody) {
    finishEditMemo();
    return;
  }

  const now = Date.now();

  state.memos = state.memos.map((memo) => {
    if (memo.id !== id) return memo;
    return {
      ...memo,
      title: trimmedTitle || "無題のメモ",
      body: trimmedBody,
      updatedAt: now,
    };
  });

  saveMemosToStorage();
  finishEditMemo();
}
JavaScript

これで、「編集したメモは updatedAt が更新される」ようになります。
getSortedMemos は updatedAt でソートしているので、
編集したメモが一覧の上の方に上がってくる、という自然な挙動になります。


編集モードの UI をタイトル+本文対応にする

編集モードの描画を更新する

function renderMemoItemEditing(itemEl, memo) {
  const titleInputEl = document.createElement("input");
  titleInputEl.value = memo.title;
  titleInputEl.style.width = "100%";
  titleInputEl.placeholder = "タイトル(任意)";

  const bodyTextareaEl = document.createElement("textarea");
  bodyTextareaEl.value = memo.body;
  bodyTextareaEl.style.width = "100%";
  bodyTextareaEl.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", () => {
    updateMemo(memo.id, titleInputEl.value, bodyTextareaEl.value);
  });

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

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

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

  itemEl.appendChild(titleInputEl);
  itemEl.appendChild(bodyTextareaEl);
  itemEl.appendChild(actionsEl);

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

ここでのポイントは、
タイトルと本文を別々の入力欄として扱っていること。
保存時に両方の値を updateMemo に渡していること。
Esc や Ctrl+Enter の UX はそのまま活かしていること。

「データ構造をリッチにしたら、UI もそれに合わせて素直に分解する」
という感覚が、設計としてとても大事です。


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

4日目であなたがやったのは、「永続化されるデータの設計を一段引き上げる」ことでした。

メモ 1 件を「タイトル+本文+更新日時」という形で設計し直したこと。
state.memos の 1 件の形を「localStorage に保存されるデータ構造」として意識したこと。
読み込み時に古い形式のデータを新しい形に変換する処理を入れたこと。
updatedAt を使って「新しい順」に並べる設計にしたこと。
編集時に updatedAt を更新し、「最近触ったメモが上に来る」自然な挙動を作ったこと。

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

検索やフィルタ(タイトルで絞り込みなど)を設計してみる。
メモ数が増えたときの見せ方(折りたたみ、要約表示など)を考える。
localStorage の容量や限界を意識した設計の話を少しだけする。

という方向に進めていきます。

もし余力があれば、今日は
実際にタイトル付きのメモをいくつか作って、編集して、並び順がどう変わるかを眺めてみてください。
「データ構造を変えると、アプリの性格が変わる」という感覚が、かなりはっきり見えてくるはずです。

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